@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
@@ -18,10 +18,24 @@
18
18
  * the actor-scoped stream has no shared-write race.
19
19
  *
20
20
  * Hot-path ordering + bounded read (F3, #288 fix iteration 1, sec-MED +
21
- * cr-MED): steps run enabled? -> current.json active_slug present? ->
22
- * resolveActor() -> bounded tail read, in that order, so a repo with liveness
23
- * disabled or no active session never pays the `resolveActor()` process-
24
- * ancestry `ps` spawn cost.
21
+ * cr-MED; restored by F4, fix-plan iteration 1): steps run enabled? ->
22
+ * mightHaveActiveSession() peek -> resolveActor() -> current.json
23
+ * active_slug present? -> bounded tail read, in that order, so a repo with
24
+ * liveness disabled OR no active session never pays the `resolveActor()`
25
+ * process-ancestry `ps` spawn cost. (#291 initially moved resolveActor()
26
+ * ahead of the active_slug check unconditionally, since the current-pointer
27
+ * read became actor-aware and needs the resolved actor to choose per-actor
28
+ * vs. legacy `current.json` — this silently dropped the "no active session"
29
+ * half of the short-circuit for every liveness-enabled call, not just the
30
+ * "disabled" half. F4 restores it: `mightHaveActiveSession()` is a plain,
31
+ * actor-free `fs.existsSync` peek of the legacy `current.json` file and the
32
+ * `current/` per-actor directory — when NEITHER exists, no actor could
33
+ * possibly have an active session (per-actor files only ever live under
34
+ * `current/`), so resolveActor() is skipped entirely, exactly restoring the
35
+ * pre-#291 short-circuit for the common "enabled, no session yet" case. Only
36
+ * once a current-pointer file might exist does resolveActor() run, still
37
+ * exactly once, reused for both the actor-scoped current-pointer read and the
38
+ * actor-unresolved gate below.)
25
39
  *
26
40
  * Orphan-heartbeat invariant (F8(ii), #288 fix iteration 2): a heartbeat must
27
41
  * never be treated as its own evidence of a claim. The reviewer-reproduced
@@ -74,12 +88,12 @@
74
88
 
75
89
  const fs = require('fs');
76
90
  const path = require('path');
77
-
78
91
  const { isLivenessEnabled, resolveHeartbeatThrottleSeconds } = require('./liveness-policy');
79
92
  const { livenessStreamFile, appendLivenessEvent } = require('./liveness-write');
80
93
  const { readLivenessEvents, readLivenessEventsTail, freshHolders } = require('./liveness-read');
81
94
  const { resolveActor, isUnresolvedActor, sanitizeSegment } = require('./actor-identity');
82
95
  const { flowAgentsArtifactRoot } = require('./local-artifact-paths');
96
+ const { readCurrentPointer } = require('./current-pointer');
83
97
 
84
98
  /**
85
99
  * Resolve a caller-supplied `now` (Date, ISO string, or omitted) to epoch ms.
@@ -101,35 +115,50 @@ function resolveNowMs(now) {
101
115
  }
102
116
 
103
117
  /**
104
- * Read `current.json`'s `active_slug` from the given artifact root, sanitized through the same
105
- * charset+cap restriction as actor-identity.js's `sanitizeSegment` (F5, #288 fix iteration 1,
106
- * sec-LOW: `current.json` is a local file that could be hand-edited or otherwise hostile
107
- * `active_slug` must never be trusted verbatim before it is used as a JSONL grouping key /
108
- * emitted `subjectId` or compared against event data). Tolerates a missing file or malformed
109
- * JSON (returns "").
118
+ * Read the active_slug from the actor-scoped "current" pointer (#291: per-actor
119
+ * `current/<actor>.json` preferred over the legacy global `current.json`, via
120
+ * `readCurrentPointer` the single choke point for that preference rule), sanitized through the
121
+ * same charset+cap restriction as actor-identity.js's `sanitizeSegment` (F5, #288 fix iteration 1,
122
+ * sec-LOW: this is a local file that could be hand-edited or otherwise hostile — `active_slug`
123
+ * must never be trusted verbatim before it is used as a JSONL grouping key / emitted `subjectId`
124
+ * or compared against event data). Tolerates a missing file or malformed JSON (returns "") exactly
125
+ * like the plain-`fs.readFileSync` read this replaces; when `actorKey` is empty/unresolved,
126
+ * `readCurrentPointer` falls straight to the legacy global file, so behavior for that case is
127
+ * unchanged from before #291.
110
128
  *
111
129
  * @param {string} root
130
+ * @param {string} [actorKey]
112
131
  * @returns {string}
113
132
  */
114
- function readActiveSlug(root) {
115
- let raw = '';
116
- try {
117
- raw = fs.readFileSync(path.join(root, 'current.json'), 'utf8');
118
- } catch {
119
- return '';
120
- }
121
- try {
122
- const parsed = JSON.parse(raw);
123
- const activeSlug = (parsed && parsed.active_slug) || '';
124
- if (!activeSlug) return '';
125
- // sanitizeSegment falls back to the literal "unknown" for an all-stripped input; that
126
- // fallback is not a legitimate slug, so treat it as "no active slug" here rather than
127
- // matching against a subject that was never really claimed.
128
- const sanitized = sanitizeSegment(activeSlug);
129
- return sanitized === 'unknown' ? '' : sanitized;
130
- } catch {
131
- return '';
132
- }
133
+ /**
134
+ * F4 fix (fix-plan iteration 1, MED): a cheap, actor-free existence peek restoring the module's
135
+ * documented "no active session never pays resolveActor()" invariant. `readCurrentPointer`'s
136
+ * actor-aware preference genuinely needs a resolved actor to choose per-actor vs. legacy — but
137
+ * that choice is only ever relevant when SOME current-pointer file might exist at all. When
138
+ * neither the legacy global `<root>/current.json` file NOR the `<root>/current/` per-actor
139
+ * directory exists, there is provably no active session for any actor (per-actor files are only
140
+ * ever written into that directory — see current-pointer.js's writePerActorCurrent), so this
141
+ * function returns false without calling resolveActor() at all, exactly restoring the pre-#291
142
+ * short-circuit for the common "liveness enabled, no session yet" case. Two plain fs.existsSync
143
+ * calls — no ps/proc ancestry cost, no JSON parse.
144
+ *
145
+ * @param {string} root
146
+ * @returns {boolean}
147
+ */
148
+ function mightHaveActiveSession(root) {
149
+ return fs.existsSync(path.join(root, 'current.json')) || fs.existsSync(path.join(root, 'current'));
150
+ }
151
+
152
+ function readActiveSlug(root, actorKey) {
153
+ const { payload: current } = readCurrentPointer(root, actorKey);
154
+ if (!current) return '';
155
+ const activeSlug = (current && current.active_slug) || '';
156
+ if (!activeSlug) return '';
157
+ // sanitizeSegment falls back to the literal "unknown" for an all-stripped input; that
158
+ // fallback is not a legitimate slug, so treat it as "no active slug" here rather than
159
+ // matching against a subject that was never really claimed.
160
+ const sanitized = sanitizeSegment(activeSlug);
161
+ return sanitized === 'unknown' ? '' : sanitized;
133
162
  }
134
163
 
135
164
  /**
@@ -221,9 +250,10 @@ function attachConflictField(result, conflict) {
221
250
  * object, never throws.
222
251
  *
223
252
  * See the module header for the full F8(ii) throttle/emit split. Summary:
224
- * - enabled? -> slug present? -> resolveActor() -> bounded tail read, in
225
- * that order (F3), so a disabled repo or one with no active session
226
- * never pays the resolveActor() ancestry `ps` spawn cost.
253
+ * - enabled? -> mightHaveActiveSession() peek -> resolveActor() -> slug
254
+ * present? -> bounded tail read, in that order (F3, restored by F4), so
255
+ * a disabled repo or one with no active session never pays the
256
+ * resolveActor() ancestry `ps` spawn cost.
227
257
  * - THROTTLE decision reads only the bounded tail (never a full read):
228
258
  * a release found there refuses; a fresh event there throttles.
229
259
  * - EMIT decision requires an actual `claim` event for the pair: taken
@@ -248,12 +278,22 @@ function maybeEmitHeartbeat(opts = {}) {
248
278
  }
249
279
 
250
280
  const root = flowAgentsArtifactRoot(cwd);
251
- const slug = readActiveSlug(root);
252
- if (!slug) {
281
+ // F4 fix (fix-plan iteration 1, MED): restore the "no active session never pays
282
+ // resolveActor()" invariant. mightHaveActiveSession() is a plain fs.existsSync peek of the
283
+ // LEGACY current.json / current/ dir — no actor needed — so a repo with liveness enabled but
284
+ // no session at all (the common case this invariant exists for) short-circuits here, exactly
285
+ // like pre-#291. Only once a current-pointer file MIGHT exist do we pay for resolveActor(),
286
+ // needed to decide per-actor vs. legacy preference via readActiveSlug/readCurrentPointer.
287
+ if (!mightHaveActiveSession(root)) {
253
288
  return { emitted: false, reason: 'no-current' };
254
289
  }
255
290
 
256
291
  const { actor } = resolveActor(env);
292
+ const slug = readActiveSlug(root, actor);
293
+ if (!slug) {
294
+ return { emitted: false, reason: 'no-current' };
295
+ }
296
+
257
297
  if (isUnresolvedActor(actor)) {
258
298
  return { emitted: false, reason: 'actor-unresolved' };
259
299
  }
@@ -42,6 +42,8 @@ const {
42
42
  flowAgentsArtifactRoot,
43
43
  flowAgentsArtifactRootsForRead,
44
44
  } = require('./lib/local-artifact-paths');
45
+ const { resolveActor } = require('./lib/actor-identity.js');
46
+ const { readCurrentPointer } = require('./lib/current-pointer.js');
45
47
 
46
48
  const MAX_STDIN = 1024 * 1024;
47
49
  const ACTIVE_STATUSES = new Set([
@@ -1531,7 +1533,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
1531
1533
  * to scanning all of .kontourai/flow-agents (newest-mtime).
1532
1534
  */
1533
1535
  function preferredArtifactDir(flowAgentsDir) {
1534
- const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
1536
+ const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
1535
1537
  if (!current) return null;
1536
1538
  const slug = current.artifact_dir || current.active_slug;
1537
1539
  if (typeof slug !== 'string' || !slug.trim()) return null;
@@ -1549,7 +1551,7 @@ function hasSidecarPresence(artifactDir) {
1549
1551
  // return that slug so analyze() can log the staleness rather than silently falling back to
1550
1552
  // a global mtime scan that could resurface an abandoned/never-real session as active.
1551
1553
  function staleCurrentSlug(flowAgentsDir) {
1552
- const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
1554
+ const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
1553
1555
  if (!current) return null;
1554
1556
  const slug = current.artifact_dir || current.active_slug;
1555
1557
  if (typeof slug !== 'string' || !slug.trim()) return null;
@@ -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 => {
@@ -10,6 +10,8 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const { resolveActor } = require('../hooks/lib/actor-identity.js');
14
+ const { readCurrentPointer } = require('../hooks/lib/current-pointer.js');
13
15
 
14
16
  const MAX_SUMMARY = 72;
15
17
 
@@ -76,7 +78,7 @@ function stateFiles(workflowRoot) {
76
78
  }
77
79
 
78
80
  function chooseWorkflow(workflowRoot) {
79
- const current = readJson(path.join(workflowRoot, 'current.json'));
81
+ const { payload: current } = readCurrentPointer(workflowRoot, resolveActor(process.env).actor);
80
82
  if (current?.active_slug || current?.artifact_dir) {
81
83
  const slug = current.active_slug || current.artifact_dir;
82
84
  const dir = path.join(workflowRoot, slug);