@kontourai/flow-agents 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.github/workflows/ci.yml +12 -0
  2. package/CHANGELOG.md +32 -0
  3. package/CONTEXT.md +67 -1
  4. package/README.md +4 -0
  5. package/build/src/cli/assignment-provider.d.ts +188 -0
  6. package/build/src/cli/assignment-provider.js +193 -51
  7. package/build/src/cli/workflow-sidecar.d.ts +16 -6
  8. package/build/src/cli/workflow-sidecar.js +492 -45
  9. package/build/src/lib/flow-resolver.d.ts +12 -6
  10. package/build/src/lib/flow-resolver.js +30 -14
  11. package/build/src/tools/validate-source-tree.js +2 -1
  12. package/context/contracts/assignment-provider-contract.md +10 -1
  13. package/context/contracts/execution-contract.md +78 -0
  14. package/context/contracts/probe-docs-write-contract.md +187 -0
  15. package/context/scripts/hooks/config-protection.js +24 -4
  16. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +262 -5
  18. package/context/scripts/hooks/workflow-steering.js +42 -0
  19. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  20. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  21. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  22. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  23. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  24. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  25. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  26. package/docs/adr/0007-skill-audit.md +2 -0
  27. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  28. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  29. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  30. package/docs/adr/0011-mcp-posture.md +2 -0
  31. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  32. package/docs/adr/0013-context-lifecycle.md +2 -0
  33. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  34. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  35. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  36. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  37. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  38. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  39. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  40. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  41. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
  42. package/docs/adr/README.md +49 -0
  43. package/docs/adr/index.md +34 -0
  44. package/docs/decisions/agent-coordination.md +20 -0
  45. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  46. package/docs/decisions/context-lifecycle.md +18 -0
  47. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  48. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  49. package/docs/decisions/flow-kit.md +20 -0
  50. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  51. package/docs/decisions/graph-knowledge-provider.md +63 -0
  52. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  53. package/docs/decisions/index.md +19 -0
  54. package/docs/decisions/kit-dependency-ownership.md +18 -0
  55. package/docs/decisions/kit-operation-boundary.md +18 -0
  56. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  57. package/docs/decisions/kontour-resource-contract.md +18 -0
  58. package/docs/decisions/mcp-posture.md +18 -0
  59. package/docs/decisions/three-hard-boundary-model.md +18 -0
  60. package/docs/decisions/trust-reconcile.md +20 -0
  61. package/docs/decisions/typescript-source-policy.md +48 -0
  62. package/docs/decisions/workflow-enforcement.md +18 -0
  63. package/docs/decisions/workflow-trust-state.md +20 -0
  64. package/docs/fixture-ownership.md +1 -1
  65. package/docs/workflow-usage-guide.md +1 -1
  66. package/evals/ci/run-baseline.sh +6 -0
  67. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  68. package/evals/integration/test_checkpoint_signing.sh +4 -3
  69. package/evals/integration/test_current_json_per_actor.sh +516 -0
  70. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  71. package/evals/integration/test_gate_lockdown.sh +46 -0
  72. package/evals/integration/test_model_routing_escalation.sh +145 -0
  73. package/evals/integration/test_publish_delivery.sh +14 -6
  74. package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
  75. package/evals/integration/test_stop_hook_release.sh +552 -0
  76. package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
  77. package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
  78. package/evals/run.sh +10 -0
  79. package/evals/static/test_knowledge_providers.sh +13 -4
  80. package/evals/static/test_model_routing_hints.sh +107 -0
  81. package/evals/static/test_workflow_skills.sh +15 -2
  82. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  83. package/kits/builder/skills/deliver/SKILL.md +69 -11
  84. package/kits/builder/skills/design-probe/SKILL.md +47 -0
  85. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  86. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  87. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  88. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  89. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  90. package/kits/builder/skills/pull-work/SKILL.md +10 -0
  91. package/kits/builder/skills/review-work/SKILL.md +11 -0
  92. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  93. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  94. package/kits/knowledge/adapters/default-store/index.js +147 -18
  95. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  96. package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
  97. package/kits/knowledge/adapters/shared/codec.js +265 -0
  98. package/kits/knowledge/docs/README.md +121 -2
  99. package/kits/knowledge/docs/store-contract.md +517 -7
  100. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  101. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  102. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  103. package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
  104. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  105. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  106. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  107. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  108. package/kits/knowledge/flows/promote.flow.json +84 -0
  109. package/kits/knowledge/kit.json +15 -0
  110. package/kits/knowledge/promote/distill.js +96 -0
  111. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  112. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  113. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  114. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  115. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  116. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  117. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  118. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  119. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  120. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  121. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  122. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  123. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  124. package/kits/knowledge/promote/health.js +137 -0
  125. package/kits/knowledge/promote/index.js +176 -0
  126. package/kits/knowledge/promote/ingest.js +103 -0
  127. package/kits/knowledge/promote/lib.js +132 -0
  128. package/kits/knowledge/promote/link.js +84 -0
  129. package/kits/knowledge/promote/promote.test.js +174 -0
  130. package/kits/knowledge/providers/conformance/suite.test.js +18 -0
  131. package/kits/knowledge/providers/index.js +1 -0
  132. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  133. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  134. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  135. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  136. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  137. package/kits/knowledge/providers/neo4j/index.js +280 -0
  138. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  139. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  140. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  141. package/package.json +4 -2
  142. package/schemas/workflow-handoff.schema.json +6 -0
  143. package/scripts/README.md +1 -0
  144. package/scripts/ci/mint-attestation.js +33 -6
  145. package/scripts/ci/trust-reconcile.js +144 -26
  146. package/scripts/freeze-adrs.mjs +364 -0
  147. package/scripts/hooks/config-protection.js +24 -4
  148. package/scripts/hooks/evidence-capture.js +4 -1
  149. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  150. package/scripts/hooks/lib/current-pointer.js +123 -0
  151. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  152. package/scripts/hooks/stop-goal-fit.js +262 -5
  153. package/scripts/hooks/workflow-steering.js +42 -0
  154. package/scripts/statusline/flow-agents-statusline.js +3 -1
  155. package/src/cli/assignment-provider.ts +239 -59
  156. package/src/cli/workflow-sidecar.ts +529 -43
  157. package/src/lib/flow-resolver.ts +35 -14
  158. package/src/tools/validate-source-tree.ts +2 -1
@@ -25,6 +25,7 @@
25
25
  'use strict';
26
26
 
27
27
  const fs = require('fs');
28
+ const os = require('os');
28
29
  const path = require('path');
29
30
  const { spawnSync } = require('child_process');
30
31
  const crypto = require('crypto');
@@ -42,6 +43,8 @@ const {
42
43
  flowAgentsArtifactRoot,
43
44
  flowAgentsArtifactRootsForRead,
44
45
  } = require('./lib/local-artifact-paths');
46
+ const { resolveActor, isUnresolvedActor, detectRuntime } = require('./lib/actor-identity.js');
47
+ const { readCurrentPointer } = require('./lib/current-pointer.js');
45
48
 
46
49
  const MAX_STDIN = 1024 * 1024;
47
50
  const ACTIVE_STATUSES = new Set([
@@ -1531,7 +1534,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
1531
1534
  * to scanning all of .kontourai/flow-agents (newest-mtime).
1532
1535
  */
1533
1536
  function preferredArtifactDir(flowAgentsDir) {
1534
- const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
1537
+ const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
1535
1538
  if (!current) return null;
1536
1539
  const slug = current.artifact_dir || current.active_slug;
1537
1540
  if (typeof slug !== 'string' || !slug.trim()) return null;
@@ -1549,7 +1552,7 @@ function hasSidecarPresence(artifactDir) {
1549
1552
  // return that slug so analyze() can log the staleness rather than silently falling back to
1550
1553
  // a global mtime scan that could resurface an abandoned/never-real session as active.
1551
1554
  function staleCurrentSlug(flowAgentsDir) {
1552
- const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
1555
+ const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
1553
1556
  if (!current) return null;
1554
1557
  const slug = current.artifact_dir || current.active_slug;
1555
1558
  if (typeof slug !== 'string' || !slug.trim()) return null;
@@ -1669,7 +1672,7 @@ async function analyze(root, now = Date.now()) {
1669
1672
  artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
1670
1673
  }
1671
1674
 
1672
- if (artifacts.length === 0) return { warnings: [], blocking: false };
1675
+ if (artifacts.length === 0) return { warnings: [], blocking: false, latestArtifactDir: null };
1673
1676
 
1674
1677
  const latest = artifacts[0];
1675
1678
  const latestArtifactDir = path.dirname(latest.file);
@@ -1743,7 +1746,7 @@ async function analyze(root, now = Date.now()) {
1743
1746
  if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
1744
1747
  return blockRe.test(w);
1745
1748
  });
1746
- return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep) };
1749
+ return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
1747
1750
  }
1748
1751
 
1749
1752
  /**
@@ -1842,12 +1845,266 @@ function remediationFor(warning) {
1842
1845
  return null;
1843
1846
  }
1844
1847
 
1848
+ // ─── #292 Wave 2: Stop-hook non-terminal release-with-handoff ────────────────
1849
+ //
1850
+ // When a session's Stop event fires and the session's state.json status is NOT
1851
+ // one of workflow-sidecar.ts's LIVENESS_TERMINAL statuses (delivered/accepted/
1852
+ // archived — the SAME set advance-state's terminal path already tests against,
1853
+ // imported here rather than re-declared, per AC8), the liveness claim and (for
1854
+ // the local-file assignment provider) the durable assignment claim would
1855
+ // otherwise sit held until TTL, even though the session has genuinely ended.
1856
+ // This closes that gap: always emit a provider-agnostic liveness release (AC1),
1857
+ // and for local-file additionally perform the real release inline (AC2), while
1858
+ // honestly disclosing (never executing) the equivalent GitHub-provider release
1859
+ // as a pending handoff.json intent (AC3). See the plan artifact
1860
+ // (.kontourai/flow-agents/kontourai-flow-agents-292/kontourai-flow-agents-292--plan-work.md,
1861
+ // "Wave 2") for the full design rationale and the render-don't-execute
1862
+ // constraint this deliberately does not cross.
1863
+ //
1864
+ // Fail-open (AC7): the ENTIRE body runs under one try/catch. Any failure —
1865
+ // missing build/, corrupt assignment record, unresolved actor, missing/
1866
+ // malformed provider settings — degrades to a single stderr diagnostic and
1867
+ // returns, never throwing, never affecting the Stop hook's own exit code or
1868
+ // goal-fit's warnings/blocking output (this function's return value is not
1869
+ // consumed by that contract at all — it is a pure side effect).
1870
+
1871
+ /**
1872
+ * require() the compiled workflow-sidecar.js the same hasBuild/fs.existsSync-guarded
1873
+ * way loadActiveFlowStep() already requires flow-resolver.js. Returns null (never
1874
+ * throws) when build/ is absent or the require fails — the caller treats null as
1875
+ * "the LIVENESS_TERMINAL boundary is unknown; do not guess it" and skips the whole
1876
+ * release (fail-open, never a re-derived duplicate Set — AC8).
1877
+ */
1878
+ function loadWorkflowSidecarBuilt() {
1879
+ const packageRoot = path.resolve(__dirname, '..', '..');
1880
+ const builtSidecar = path.join(packageRoot, 'build', 'src', 'cli', 'workflow-sidecar.js');
1881
+ if (!fs.existsSync(builtSidecar)) return null; // hasBuild guard
1882
+ try {
1883
+ const mod = require(builtSidecar);
1884
+ if (!(mod.LIVENESS_TERMINAL instanceof Set)) return null;
1885
+ return mod;
1886
+ } catch {
1887
+ return null; // require failed — fail-open
1888
+ }
1889
+ }
1890
+
1891
+ /** Same hasBuild-guarded require idiom, for build/src/cli/assignment-provider.js. */
1892
+ function loadAssignmentProviderBuilt() {
1893
+ const packageRoot = path.resolve(__dirname, '..', '..');
1894
+ const builtProvider = path.join(packageRoot, 'build', 'src', 'cli', 'assignment-provider.js');
1895
+ if (!fs.existsSync(builtProvider)) return null;
1896
+ try {
1897
+ const mod = require(builtProvider);
1898
+ if (typeof mod.performLocalRelease !== 'function') return null;
1899
+ return mod;
1900
+ } catch {
1901
+ return null;
1902
+ }
1903
+ }
1904
+
1905
+ /**
1906
+ * Resolve the effective assignment-provider kind for this repo ('local-file' | 'github'),
1907
+ * or null when it cannot be determined. Prefers reading context/settings/
1908
+ * assignment-provider-settings.json directly (project settings first, matching
1909
+ * effective-assignment-provider-settings.ts's own lookup precedence) rather than shelling
1910
+ * out to or refactoring that CLI-shaped file (see plan Unresolved Question 1) — this task's
1911
+ * scope is the Stop hook, not that file. Returns null (never guesses) when no settings file
1912
+ * is present/parseable or names no provider kind for a project entry.
1913
+ *
1914
+ * Deliberately reads ONLY the project settings file (context/settings/
1915
+ * assignment-provider-settings.json relative to repo root) — the common case for this repo
1916
+ * and the one confirmed present. The effective() merge/repo-matching logic in
1917
+ * effective-assignment-provider-settings.ts is intentionally NOT reimplemented here (that
1918
+ * would fork a second merge algorithm); a repo whose provider kind can only be resolved via
1919
+ * global settings or multi-project merge degrades to "unknown, skip assignment-layer
1920
+ * release" rather than guessing.
1921
+ */
1922
+ function resolveAssignmentProviderKind(root) {
1923
+ const settingsFile = path.join(root, 'context', 'settings', 'assignment-provider-settings.json');
1924
+ const settings = readJsonFile(settingsFile);
1925
+ if (!settings) return null;
1926
+ const candidates = [];
1927
+ if (settings.defaults && settings.defaults.provider) candidates.push(settings.defaults.provider);
1928
+ if (Array.isArray(settings.projects)) {
1929
+ for (const project of settings.projects) {
1930
+ if (project && project.provider) candidates.push(project.provider);
1931
+ }
1932
+ }
1933
+ const kind = candidates.map(p => p && p.kind).find(k => k === 'local-file' || k === 'github');
1934
+ return kind || null;
1935
+ }
1936
+
1937
+ /**
1938
+ * Refresh handoff.json by merge (read+spread the existing file's JSON, overwrite only the
1939
+ * fields this task owns), mirroring advanceState's own handoff-write shape
1940
+ * (`{ ...loadJson(...), ...sidecarBase(slug), summary, current_state_ref: "state.json",
1941
+ * next_steps, blockers: [], warnings: [] }`) and current-pointer.js's writePerActorCurrent
1942
+ * write idiom (`fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n")`). next_steps
1943
+ * is appended to (not clobbered) when a provider-release next-step is due.
1944
+ */
1945
+ function refreshHandoffAfterRelease(artifactDir, slug, state, providerReleasePending, providerReleaseNextCommand) {
1946
+ const file = path.join(artifactDir, 'handoff.json');
1947
+ const existing = readJsonFile(file) || {};
1948
+ const status = state ? normalizedStatus(state.status || 'unknown') : 'unknown';
1949
+ const phase = state ? normalizedStatus(state.phase || 'unknown') : 'unknown';
1950
+ const summary = (state && state.next_action && state.next_action.summary)
1951
+ ? String(state.next_action.summary)
1952
+ : `session ended at status:${status} phase:${phase}`;
1953
+ const existingNextSteps = Array.isArray(existing.next_steps) ? existing.next_steps.slice() : [];
1954
+ const nextSteps = existingNextSteps.slice();
1955
+ if (providerReleasePending && providerReleaseNextCommand && !nextSteps.includes(providerReleaseNextCommand)) {
1956
+ nextSteps.push(providerReleaseNextCommand);
1957
+ }
1958
+ const payload = {
1959
+ ...existing,
1960
+ schema_version: existing.schema_version || '1.0',
1961
+ task_slug: existing.task_slug || slug,
1962
+ summary,
1963
+ current_state_ref: 'state.json',
1964
+ next_steps: nextSteps,
1965
+ ...(providerReleasePending ? { provider_release_pending: true } : {}),
1966
+ ...(providerReleaseNextCommand ? { provider_release_next_command: providerReleaseNextCommand } : {}),
1967
+ };
1968
+ fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`);
1969
+ }
1970
+
1971
+ /**
1972
+ * #292 Wave 2: on a Stop event whose resolved session is at a NON-terminal status, release
1973
+ * the session's liveness claim (always, provider-agnostic — AC1) and, for the local-file
1974
+ * assignment provider, its durable assignment claim too (AC2), then refresh handoff.json
1975
+ * (AC4). A terminal-status session (already released by advance-state) is a deliberate no-op
1976
+ * (AC5). For the github provider, never calls render-release or gh — only records an
1977
+ * honestly-labeled pending intent in handoff.json (AC3). Actor-scoped: only ever releases
1978
+ * the CURRENT actor's own held claim (AC6, enforced inside performLocalRelease).
1979
+ *
1980
+ * Called once from run(), reusing analyze()'s already-resolved latestArtifactDir — this
1981
+ * function does not re-derive "what is the active session" a second time.
1982
+ *
1983
+ * Fail-open (AC7): the entire body is wrapped in one try/catch. Any failure degrades to a
1984
+ * single stderr diagnostic and returns, never throwing, never affecting the Stop hook's exit
1985
+ * code or goal-fit's warnings/blocking contract (this function's return value is unused by
1986
+ * that contract).
1987
+ *
1988
+ * @param {string} root - repo root (as resolved by findRepoRoot)
1989
+ * @param {string|null} artifactDir - analyze()'s already-resolved latestArtifactDir
1990
+ */
1991
+ function releaseOnNonTerminalStop(root, artifactDir) {
1992
+ try {
1993
+ if (!artifactDir) return; // no active session resolved — nothing to release.
1994
+
1995
+ const state = readJsonFile(path.join(artifactDir, 'state.json'));
1996
+ if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
1997
+
1998
+ const sidecar = loadWorkflowSidecarBuilt();
1999
+ if (!sidecar) {
2000
+ process.stderr.write('[Hook] Goal Fit: stop-hook release skipped — build/src/cli/workflow-sidecar.js not available (LIVENESS_TERMINAL boundary unknown).\n');
2001
+ return;
2002
+ }
2003
+
2004
+ const status = normalizedStatus(state.status || 'unknown');
2005
+ if (sidecar.LIVENESS_TERMINAL.has(status)) return; // AC5: terminal — advance-state already released.
2006
+
2007
+ // Slug: artifact dir's basename, or state.json's own task_slug field — reuse whichever
2008
+ // this file already has, never a new slug-derivation rule.
2009
+ const slug = (state.task_slug && String(state.task_slug).trim()) || path.basename(artifactDir);
2010
+
2011
+ const { actor } = resolveActor(process.env);
2012
+ if (isUnresolvedActor(actor)) {
2013
+ process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for "${safeOneLine(slug, 80)}" — actor identity is unresolved; never releasing under a synthetic identity.\n`);
2014
+ return;
2015
+ }
2016
+
2017
+ const flowAgentsDir = path.dirname(artifactDir);
2018
+ const nowIso = new Date().toISOString();
2019
+
2020
+ // AC1: ALWAYS emit the liveness release, regardless of provider kind — liveness is
2021
+ // provider-agnostic and this is the ONE shared writer every other liveness emitter uses.
2022
+ try {
2023
+ require('./lib/liveness-write.js').appendLivenessEvent(flowAgentsDir, {
2024
+ type: 'release',
2025
+ subjectId: slug,
2026
+ actor,
2027
+ at: nowIso,
2028
+ source: 'stop-hook',
2029
+ });
2030
+ } catch (err) {
2031
+ process.stderr.write(`[Hook] Goal Fit: stop-hook liveness release failed for "${safeOneLine(slug, 80)}": ${safeOneLine(err && err.message || err, 160)}\n`);
2032
+ }
2033
+
2034
+ // Resolve the effective assignment-provider kind — unknown means "do not guess",
2035
+ // skip the assignment-layer release entirely (fail-open).
2036
+ const providerKind = resolveAssignmentProviderKind(root);
2037
+ if (!providerKind) {
2038
+ process.stderr.write(`[Hook] Goal Fit: stop-hook release — provider kind unknown for "${safeOneLine(slug, 80)}", skipping assignment-layer release (liveness release already emitted).\n`);
2039
+ refreshHandoffAfterRelease(artifactDir, slug, state, false, null);
2040
+ return;
2041
+ }
2042
+
2043
+ let providerReleasePending = false;
2044
+ let providerReleaseNextCommand = null;
2045
+
2046
+ if (providerKind === 'local-file') {
2047
+ const provider = loadAssignmentProviderBuilt();
2048
+ if (!provider) {
2049
+ process.stderr.write('[Hook] Goal Fit: stop-hook local-file release skipped — build/src/cli/assignment-provider.js not available.\n');
2050
+ } else {
2051
+ try {
2052
+ // Mirror assignment-provider.ts's loadActorStruct() exactly: runtime is derived via
2053
+ // detectRuntime (not a literal 'unknown'), since serializeActor()'s comparison key
2054
+ // includes runtime — a mismatched runtime segment here would make performLocalRelease's
2055
+ // AC6 actor-match check wrongly treat this session's own claim as foreign.
2056
+ //
2057
+ // actorKey: opts.actorKey MUST be the same canonical `resolveActor(env).actor` string
2058
+ // already resolved above (`actor`) — the identical bare-or-triple form
2059
+ // computeEffectiveState()'s holderActorKey preference (`record.actor_key ||
2060
+ // serializeActor(record.actor)`) and performLocalRelease's mirrored ownership check
2061
+ // both key on. Without this, an explicit-override actor's release-side comparison
2062
+ // would fall back to serializeActor(releasedBy) — a re-derived triple that never
2063
+ // equals the stored bare actor_key — and this hook would wrongly treat its own claim
2064
+ // as foreign (the #291 seam, relocated to the release path; see performLocalRelease's
2065
+ // doc comment in src/cli/assignment-provider.ts).
2066
+ const releasedBy = { runtime: detectRuntime(process.env), session_id: actor, host: os.hostname(), human: null };
2067
+ const artifactRoot = flowAgentsArtifactRoot(root);
2068
+ provider.performLocalRelease(artifactRoot, slug, releasedBy, {
2069
+ reason: 'stop-hook non-terminal release',
2070
+ tolerateNoActiveClaim: true,
2071
+ actorKey: actor,
2072
+ });
2073
+ } catch (err) {
2074
+ process.stderr.write(`[Hook] Goal Fit: stop-hook local-file release failed for "${safeOneLine(slug, 80)}": ${safeOneLine(err && err.message || err, 160)}\n`);
2075
+ }
2076
+ }
2077
+ } else {
2078
+ // AC3: github (or any non-local-file kind) — render-don't-execute. Never call
2079
+ // render-release, never invoke gh. Disclose the pending intent honestly.
2080
+ providerReleasePending = true;
2081
+ providerReleaseNextCommand = `npm run workflow -- assignment-provider status --provider github --subject-id "${slug}" ... # then: assignment-provider render-release --provider github --subject-id "${slug}" ... (emits the gh argv to run)`;
2082
+ }
2083
+
2084
+ // AC4: refresh handoff.json — merge, never fully overwrite.
2085
+ refreshHandoffAfterRelease(artifactDir, slug, state, providerReleasePending, providerReleaseNextCommand);
2086
+ } catch (err) {
2087
+ // Fail-open (AC7): any unexpected failure anywhere above (JSON parse error, fs error,
2088
+ // etc.) must never crash the Stop hook or affect its exit code / goal-fit's output.
2089
+ try {
2090
+ process.stderr.write(`[Hook] Goal Fit: stop-hook release logic error (fail-open): ${safeOneLine(err && err.message || err, 160)}\n`);
2091
+ } catch { /* best-effort diagnostic only */ }
2092
+ }
2093
+ }
2094
+
1845
2095
  async function run(rawInput) {
1846
2096
  const input = parseJson(rawInput);
1847
2097
  const root = findRepoRoot(input.cwd || process.cwd());
1848
2098
  const mode = resolveGoalFitMode();
1849
2099
  if (mode === 'off') return rawInput;
1850
2100
  const result = await analyze(root);
2101
+ // #292 Wave 2: additive side effect only — never changes analyze()'s warnings/blocking
2102
+ // contract or this function's return value. Reuses analyze()'s already-resolved
2103
+ // latestArtifactDir rather than re-deriving a second "what is the active session" path.
2104
+ // Runs regardless of whether goal-fit itself has any warnings this turn (AC1: the
2105
+ // liveness release must always be attempted on a non-terminal Stop, independent of
2106
+ // whether goal-fit found anything to warn about).
2107
+ releaseOnNonTerminalStop(root, result.latestArtifactDir || null);
1851
2108
  if (result.warnings.length === 0) {
1852
2109
  clearBlockStreak(root);
1853
2110
  return rawInput;
@@ -1937,4 +2194,4 @@ if (require.main === module) {
1937
2194
  });
1938
2195
  }
1939
2196
 
1940
- module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator };
2197
+ module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator, releaseOnNonTerminalStop };
@@ -19,6 +19,7 @@ const path = require('path');
19
19
  const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
20
20
  const { resolveActor } = require('./lib/actor-identity');
21
21
  const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
22
+ const { readCurrentPointer } = require('./lib/current-pointer');
22
23
 
23
24
  const STEERING = {
24
25
  'tool-planner': [
@@ -102,7 +103,48 @@ function readJson(file) {
102
103
  }
103
104
  }
104
105
 
106
+ /**
107
+ * #291 (Conflict #2, Wave 2 Task 2.4): actor-scoped preference consulted BEFORE the global
108
+ * newest-mtime scan below. For each read-root, resolve the "current" pointer via
109
+ * `readCurrentPointer` (per-actor `current/<actor>.json` preferred, legacy global
110
+ * `current.json` fallback — the same single choke point every other Wave 2 consumer uses) and,
111
+ * if it names a session dir whose `state.json` is still in an ACTIVE_STATE_STATUSES status,
112
+ * return that state immediately — so actor A's own ambient steering hint always prefers A's own
113
+ * current task over a globally-newer-mtime `state.json` some other actor (B) happened to touch
114
+ * more recently. Returns null (never throws) when no root yields an actor-scoped resolution,
115
+ * so the caller falls through to the unchanged global scan below — this is the compat-shim
116
+ * guarantee: when `actorKey` is empty/unresolved and/or no per-actor file exists yet, this
117
+ * degrades to the SAME legacy-`current.json`-or-nothing lookup the compat shim already performs
118
+ * for every other Wave 2 reader, so a single-session/CI/legacy-only fixture cannot fail to
119
+ * resolve here in a way it wouldn't already have resolved via the global scan.
120
+ *
121
+ * @param {string} root
122
+ * @param {string} actorKey
123
+ * @returns {{file: string, payload: object, mtimeMs: number}|null}
124
+ */
125
+ function actorScopedWorkflowState(root, actorKey) {
126
+ for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
127
+ const { payload: current } = readCurrentPointer(flowAgentsDir, actorKey);
128
+ if (!current) continue;
129
+ const slug = current.artifact_dir || current.active_slug;
130
+ if (typeof slug !== 'string' || !slug.trim()) continue;
131
+ const safe = slug.replace(/\.\.+/g, '').replace(/^[/\\]+/, '');
132
+ const dir = path.join(flowAgentsDir, safe);
133
+ if (!dir.startsWith(flowAgentsDir + path.sep) || !fs.existsSync(dir)) continue;
134
+ const file = path.join(dir, 'state.json');
135
+ let stat;
136
+ try { stat = fs.statSync(file); } catch { continue; }
137
+ const payload = readJson(file);
138
+ if (!payload || !ACTIVE_STATE_STATUSES.has(payload.status)) continue;
139
+ return { file, payload, mtimeMs: stat.mtimeMs };
140
+ }
141
+ return null;
142
+ }
143
+
105
144
  function latestWorkflowState(root) {
145
+ const preferred = actorScopedWorkflowState(root, resolveActor(process.env).actor);
146
+ if (preferred) return preferred;
147
+
106
148
  const states = flowAgentsArtifactRootsForRead(root)
107
149
  .flatMap(artifactRoot => walkStateFiles(artifactRoot))
108
150
  .map(file => {
@@ -2,6 +2,8 @@
2
2
  title: ADR 0001: Flow Agents Consumes Flow For Workflow Enforcement
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0001: Flow Agents Consumes Flow For Workflow Enforcement
6
8
 
7
9
  Date: 2026-05-24
@@ -2,6 +2,8 @@
2
2
  title: ADR 0002: Flow Kits As The Extension Unit
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0002: Flow Kits As The Extension Unit
6
8
 
7
9
  Flow Agents will use **Flow Kit** as the product and implementation term for installable workflow bundles, replacing the older "pack" vocabulary. A Flow Kit may contain Flow Definitions, skills, adapters, provider contracts, docs, and evals; the Kit Catalog lists available kits and installable assets, but the workflow semantics live in kit-owned Flow Definition files. This avoids overloading packaging metadata with process semantics and gives custom workflow authoring a clearer product language.
@@ -2,6 +2,8 @@
2
2
  title: ADR 0003: Flow Agents Coordinates Kits And Adapters
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0003: Flow Agents Coordinates Kits And Adapters
6
8
 
7
9
  Flow Agents will not own the builder, or knowledge workflows as core behavior. Those out-of-the-box behaviors will be extracted into normal Flow Kits that use the same manifest, Flow Definition, skill, doc, provider, and eval contracts as third-party kits. Flow Agents owns kit validation, installation, runtime adapter selection, provider wiring, status/control commands, and runtime-specific export; Runtime Adapters own target-specific integration for local agent runtimes and API/framework agents.
@@ -2,6 +2,8 @@
2
2
  title: ADR 0004: Gates Expect Hachure Trust Bundles
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0004: Gates Expect Hachure Trust Bundles
6
8
 
7
9
  Flow-backed kits will model rich gate evidence as claim expectations using the Hachure trust.bundle format rather than provider-specific requirements. A gate expectation can require `kind: "trust.bundle"`, a domain claim type such as `builder.verify.tests`, accepted trust statuses such as `verified`, and whether the expectation blocks the transition; project or runtime config maps claim types to trusted Surface producers and authority traces. This lets the Builder Kit use repo governance, command checks, CI, human decisions, or future producers without naming a specific provider in the Flow Definition.
@@ -2,6 +2,8 @@
2
2
  title: ADR 0005: Kubernetes-Inspired Kontour Resource Contracts
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0005: Kubernetes-Inspired Kontour Resource Contracts
6
8
 
7
9
  Date: 2026-05-27
@@ -2,6 +2,8 @@
2
2
  title: ADR 0006: TypeScript-First Source Policy
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0006: TypeScript-First Source Policy
6
8
 
7
9
  Date: 2026-05-31
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0007: Flow / Skill / Kit / Tool Boundary"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0007: Flow / Skill / Kit / Tool Boundary
6
8
 
7
9
  **Date:** 2026-06-15
@@ -2,6 +2,8 @@
2
2
  title: "Skill Audit 2026-06-15: Flow / Skill / Kit / Tool Boundary"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # Skill Audit: Flow / Skill / Kit / Tool Boundary
6
8
 
7
9
  **Date:** 2026-06-15
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0008: Kit Operation Boundary"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0008: Kit Operation Boundary
6
8
 
7
9
  **Date:** 2026-06-15
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0009: Canonical Hook Core/Kit Boundary"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0009: Canonical Hook Core/Kit Boundary
6
8
 
7
9
  **Date:** 2026-06-23
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0010: Workflow Trust State as a Hachure Trust Bundle"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0010: Workflow Trust State as a Hachure Trust Bundle
6
8
 
7
9
  **Date:** 2026-06-23
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0011: MCP Posture — Enforcement Stays Hooks; Surface Owns MCP Projection; No Auto-Injected Config"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0011: MCP Posture
6
8
 
7
9
  **Date:** 2026-06-24
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0012: Agent Coordination as Hachure Liveness Claims"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0012: Agent Coordination as Hachure Liveness Claims
6
8
 
7
9
  **Date:** 2026-06-24
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0013: Context Lifecycle — Workflow-Boundary Compaction, Freshness-Gated Reuse, and the Learning Split"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0013: Context Lifecycle
6
8
 
7
9
  **Date:** 2026-06-25
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0014: Flow Agents core vs domain kits — the generic/kit boundary"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0014: Flow Agents core vs domain kits
6
8
 
7
9
  **Date:** 2026-06-25
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0015: Flow / Flow Agents Boundary Reconciliation"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0015: Flow / Flow Agents Boundary Reconciliation
6
8
 
7
9
  **Date:** 2026-06-25
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0016: The Three-Hard-Boundary Model — a FlowDefinition-Driven, Kit-Agnostic Core"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0016: The Three-Hard-Boundary Model — a FlowDefinition-Driven, Kit-Agnostic Core
6
8
 
7
9
  **Date:** 2026-06-26
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0017: The Anti-Gaming Trust Security Model — Layered Defense + External CI Anchor"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0017: The Anti-Gaming Trust Security Model — Layered Defense + External CI Anchor
6
8
 
7
9
  Status: Accepted
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor
6
8
 
7
9
  Status: Accepted
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0019: Kit Dependency Ownership"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0019: Kit Dependency Ownership
6
8
 
7
9
  **Date:** 2026-07-01
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers
6
8
 
7
9
  Status: Accepted
@@ -2,6 +2,8 @@
2
2
  title: "ADR 0021: Assignment Leases and Stale-Claim Takeover"
3
3
  ---
4
4
 
5
+ > **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
6
+
5
7
  # ADR 0021: Assignment Leases and Stale-Claim Takeover
6
8
 
7
9
  **Date:** 2026-07-02
@@ -98,7 +100,7 @@ Five touchpoints, one policy:
98
100
  |---|---|---|
99
101
  | `workflow-steering.js`, every turn | read | one-line liveness digest; "superseded" interrupt for the held subject |
100
102
  | `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 |
103
+ | `ensure-session` / pickup | read + write | refuse entry to a session dir under a fresh other-actor claim; supersede-then-enter if stale (**implemented by #291**) |
102
104
  | `advance-state` + tool activity | write | heartbeat rides existing writes; no bespoke timer |
103
105
  | **publish (push / PR / merge)** | **read, blocking** | **verify-hold**: hard-stop if not the fresh, non-superseded holder |
104
106