@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
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+ // freeze-adrs.mjs — ADR freeze-and-index tooling for flow-agents.
3
+ //
4
+ // Freezes numbered ADRs under docs/adr/ as immutable history and seeds the
5
+ // topic-keyed decision registry (docs/decisions/) with `needs-decision` stubs
6
+ // that carry the frozen ADR(s) as provenance evidence.
7
+ //
8
+ // Origin & provenance: this is the portable, zero-runtime-dependency standalone
9
+ // script (node >=22, ESM) piloted in kontourai/traverse per
10
+ // https://github.com/kontourai/flow-agents/issues/314 and rolled out across the
11
+ // portfolio (traverse#17, surface#115, flow#103, survey#108, and a fifth repo). This
12
+ // copy is the flow-agents cutover (https://github.com/kontourai/flow-agents/issues/332),
13
+ // the 6th application. It carries the two rollout fixes proven in the prior
14
+ // applications (surface#115 / flow#103):
15
+ // FIX 1 (frontmatter-aware banner placement): every flow-agents ADR opens with
16
+ // a YAML frontmatter block (`---\ntitle: ...\n---`). The traverse-original
17
+ // always prepended the banner at byte 0, which would push the frontmatter
18
+ // off the literal first line. `insertBanner()` detects a leading frontmatter
19
+ // block and inserts the banner AFTER its closing `---` instead.
20
+ // FIX 2 (whole-phrase lowercasing for Title Case terms): the traverse-original
21
+ // `defaultStubBody` only lowercased the first character of a subject when
22
+ // used mid-sentence, which read fine for traverse's sentence-case subjects
23
+ // but produced "trust Bundle" for Title Case glossary terms. `lowerPhrase()`
24
+ // lowercases every simple Title Case word (^[A-Z][a-z]+$) while preserving
25
+ // acronyms (MCP), CamelCase (TypeScript-first), and symbol tokens (`/`) —
26
+ // the flow-agents vocabulary contains all three.
27
+ //
28
+ // It does NOT vendor a copy of the registry validator or schema: flow-agents is
29
+ // the source of truth for both (scripts/check-decisions.cjs,
30
+ // schemas/decision-record.schema.json, wired as `npm run check:decisions` /
31
+ // `npm run gen:decisions-index` per #316). This script points at the existing
32
+ // validator to regenerate docs/decisions/index.md.
33
+ //
34
+ // Behavior (idempotent, content-preserving):
35
+ // 1. For each docs/adr/NNNN-*.md: insert a frozen banner as the ONLY body
36
+ // change (after any leading YAML frontmatter). Skipped if already present.
37
+ // 2. Generate docs/adr/index.md deterministically. A second run is diff-clean.
38
+ // 3. For each SUBJECT_GROUPS entry, create/update a `status: needs-decision`
39
+ // topic stub in docs/decisions/<slug>.md whose evidence[] links the frozen
40
+ // ADR(s). Existing stub frontmatter (status/decided) is preserved across
41
+ // reruns; only missing evidence entries are appended.
42
+ //
43
+ // Usage: node scripts/freeze-adrs.mjs (npm run freeze:adrs)
44
+
45
+ import fs from "node:fs";
46
+ import path from "node:path";
47
+ import { fileURLToPath } from "node:url";
48
+ import { execFileSync } from "node:child_process";
49
+
50
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
51
+ const ROOT = path.resolve(__dirname, "..");
52
+ const ADR_DIR = path.join(ROOT, "docs", "adr");
53
+ const ADR_INDEX_PATH = path.join(ADR_DIR, "index.md");
54
+ const DECISIONS_DIR = path.join(ROOT, "docs", "decisions");
55
+
56
+ const FROZEN_BANNER_MARKER = "FROZEN — immutable history.";
57
+ const FROZEN_BANNER =
58
+ "> **FROZEN — immutable history.** Superseding/current decisions live in " +
59
+ "[`docs/decisions/`](../decisions/index.md). Do not edit.\n\n";
60
+
61
+ // --- Subject grouping (flow-agents-specific, reviewed by hand) --------------
62
+ //
63
+ // Each ADR's decision subject is a NOUN drawn from (or added to) CONTEXT.md's
64
+ // domain vocabulary, per context/contracts/decision-registry-contract.md's slug
65
+ // rule. Multiple ADRs answering the same subject share one stub with multiple
66
+ // evidence refs (contract: "one file per decision SUBJECT"). Groups reference
67
+ // explicit ADR *filenames* (not numbers) because 0007 has two files sharing the
68
+ // number (0007-flow-skill-kit-tool-boundary.md and 0007-skill-audit.md), which a
69
+ // number-keyed map would collide.
70
+ //
71
+ // The four existing living topics (decision-records, promotion-gate,
72
+ // knowledge-store-provider, standing-directives) are NOT touched: none of the
73
+ // 22 numbered ADRs is about those subjects, so there is no evidence-merge case
74
+ // here — every frozen ADR subject gets a fresh needs-decision stub.
75
+ const SUBJECT_GROUPS = [
76
+ { slug: "workflow-enforcement", subject: "Workflow Enforcement", adrFiles: ["0001-flow-agents-consumes-flow.md"] },
77
+ { slug: "flow-kit", subject: "Flow Kit", adrFiles: ["0002-flow-kits-as-extension-unit.md", "0003-flow-agents-coordinates-kits-and-adapters.md"] },
78
+ { slug: "workflow-trust-state", subject: "Workflow trust state", adrFiles: ["0004-gates-expect-surface-claims.md", "0010-workflow-trust-state-as-hachure-bundle.md"] },
79
+ { slug: "kontour-resource-contract", subject: "Kontour Resource Contract", adrFiles: ["0005-kubernetes-inspired-resource-contracts.md"] },
80
+ { slug: "typescript-source-policy", subject: "TypeScript-first source policy", adrFiles: ["0006-typescript-first-source-policy.md"] },
81
+ { slug: "flow-skill-kit-tool-boundary", subject: "Flow / Skill / Kit / Tool boundary", adrFiles: ["0007-flow-skill-kit-tool-boundary.md", "0007-skill-audit.md"] },
82
+ { slug: "kit-operation-boundary", subject: "Kit operation boundary", adrFiles: ["0008-kit-operation-boundary.md"] },
83
+ { slug: "hook-core-kit-boundary", subject: "Hook core/kit boundary", adrFiles: ["0009-canonical-hook-core-kit-boundary.md"] },
84
+ { slug: "mcp-posture", subject: "MCP posture", adrFiles: ["0011-mcp-posture.md"] },
85
+ { slug: "agent-coordination", subject: "Agent coordination", adrFiles: ["0012-agent-coordination-as-liveness-claims.md", "0021-assignment-leases-and-stale-claim-takeover.md"] },
86
+ { slug: "context-lifecycle", subject: "Context lifecycle", adrFiles: ["0013-context-lifecycle.md"] },
87
+ { slug: "core-domain-kit-boundary", subject: "Core vs domain kit boundary", adrFiles: ["0014-core-vs-domain-kit-boundary.md"] },
88
+ { slug: "flow-flow-agents-boundary", subject: "Flow / Flow Agents boundary", adrFiles: ["0015-flow-flow-agents-boundary-reconciliation.md"] },
89
+ { slug: "three-hard-boundary-model", subject: "Three-hard-boundary model", adrFiles: ["0016-three-hard-boundary-model.md"] },
90
+ { slug: "anti-gaming-trust-security", subject: "Anti-gaming trust security", adrFiles: ["0017-anti-gaming-trust-security-model.md", "0018-freeze-local-shell-heuristics.md"] },
91
+ { slug: "kit-dependency-ownership", subject: "Kit dependency ownership", adrFiles: ["0019-kit-dependency-ownership.md"] },
92
+ { slug: "trust-reconcile", subject: "Trust-reconcile and delivery reconciliation", adrFiles: ["0020-trust-reconcile-manifest-and-claim-classification.md", "0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md"] },
93
+ ];
94
+
95
+ function listAdrFiles() {
96
+ if (!fs.existsSync(ADR_DIR)) return [];
97
+ return fs
98
+ .readdirSync(ADR_DIR)
99
+ .filter((name) => /^\d{4}-.*\.md$/.test(name))
100
+ .sort();
101
+ }
102
+
103
+ function adrNumberFromFilename(name) {
104
+ return parseInt(name.slice(0, 4), 10);
105
+ }
106
+
107
+ // FIX 1: frontmatter-aware banner insertion. If the file opens with a YAML
108
+ // frontmatter block, insert the banner immediately after its closing `---`
109
+ // (and the blank line that follows), never before the frontmatter.
110
+ function insertBanner(raw) {
111
+ if (raw.startsWith("---\n") || raw.startsWith("---\r\n")) {
112
+ const lines = raw.split(/\r?\n/);
113
+ let end = -1;
114
+ for (let i = 1; i < lines.length; i += 1) {
115
+ if (lines[i].trim() === "---") { end = i; break; }
116
+ }
117
+ if (end !== -1) {
118
+ // Skip a single blank line after the closing fence so the banner lands
119
+ // between the frontmatter and the first heading with clean spacing.
120
+ let insertAt = end + 1;
121
+ if (insertAt < lines.length && lines[insertAt].trim() === "") insertAt += 1;
122
+ const before = lines.slice(0, insertAt).join("\n");
123
+ const after = lines.slice(insertAt).join("\n");
124
+ return `${before}\n${FROZEN_BANNER}${after}`;
125
+ }
126
+ }
127
+ return FROZEN_BANNER + raw;
128
+ }
129
+
130
+ function freezeOne(file) {
131
+ const full = path.join(ADR_DIR, file);
132
+ const raw = fs.readFileSync(full, "utf8");
133
+ if (raw.includes(FROZEN_BANNER_MARKER)) {
134
+ return { file, changed: false, raw };
135
+ }
136
+ const frozen = insertBanner(raw);
137
+ fs.writeFileSync(full, frozen, "utf8");
138
+ return { file, changed: true, raw: frozen };
139
+ }
140
+
141
+ // Best-effort title/date extraction (index display only — never touches frozen
142
+ // ADR content). Widened for flow-agents' header forms: `# ADR NNNN: Title`
143
+ // headings and `Date:` / `**Date:**` / parenthesized `(YYYY-MM-DD)` date lines.
144
+ function parseAdrMeta(file, raw) {
145
+ const number = adrNumberFromFilename(file);
146
+ const lines = raw.split(/\r?\n/);
147
+ let title = file;
148
+ for (const line of lines) {
149
+ const m = line.match(/^#\s+(.*)$/);
150
+ if (m) {
151
+ title = m[1].trim().replace(/^ADR\s+\d+\s*[:—-]\s*/, "");
152
+ break;
153
+ }
154
+ }
155
+ const dateMatch =
156
+ raw.match(/^\s*\*{0,2}Date:?\*{0,2}\s*(\d{4}-\d{2}-\d{2})/m) ||
157
+ raw.match(/\((\d{4}-\d{2}-\d{2})\)/);
158
+ const date = dateMatch ? dateMatch[1] : null;
159
+ return { number, file, title, date };
160
+ }
161
+
162
+ function renderAdrIndex(entries) {
163
+ const sorted = [...entries].sort((a, b) => (a.number - b.number) || a.file.localeCompare(b.file));
164
+ const lines = [];
165
+ lines.push("---");
166
+ lines.push("title: ADR Index");
167
+ lines.push("---");
168
+ lines.push("");
169
+ lines.push("# ADR Index");
170
+ lines.push("");
171
+ lines.push("Generated by `node scripts/freeze-adrs.mjs` (`npm run freeze:adrs`). Do not edit by hand.");
172
+ lines.push(
173
+ "Numbered ADRs below are FROZEN immutable history (see the banner on each " +
174
+ "file). Current and superseding decisions live in " +
175
+ "[docs/decisions/](../decisions/index.md); a frozen ADR's subject is " +
176
+ "carried forward there as a `needs-decision` stub or a ratified decision."
177
+ );
178
+ lines.push("");
179
+ lines.push("| Number | Title | Date | Link |");
180
+ lines.push("| --- | --- | --- | --- |");
181
+ for (const e of sorted) {
182
+ const num = String(e.number).padStart(4, "0");
183
+ const date = e.date || "unknown";
184
+ lines.push(`| ${num} | ${e.title} | ${date} | [${e.file}](./${e.file}) |`);
185
+ }
186
+ lines.push("");
187
+ return lines.join("\n");
188
+ }
189
+
190
+ function readExistingStub(slug) {
191
+ const full = path.join(DECISIONS_DIR, `${slug}.md`);
192
+ if (!fs.existsSync(full)) return null;
193
+ const raw = fs.readFileSync(full, "utf8");
194
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
195
+ if (!fmMatch) return null;
196
+ const [, fmBlock, body] = fmMatch;
197
+ const statusMatch = fmBlock.match(/^status:\s*(.+)$/m);
198
+ const decidedMatch = fmBlock.match(/^decided:\s*(.+)$/m);
199
+ const evidence = [];
200
+ const evidenceBlockMatch = fmBlock.match(/^evidence:\n([\s\S]*?)(?:\n[a-z_]+:|$)/m);
201
+ if (evidenceBlockMatch) {
202
+ const itemRe = /-\s*kind:\s*(\S+)\n\s*ref:\s*(.+)/g;
203
+ let m;
204
+ while ((m = itemRe.exec(evidenceBlockMatch[1])) !== null) {
205
+ evidence.push({ kind: m[1].trim(), ref: m[2].trim() });
206
+ }
207
+ }
208
+ return {
209
+ status: statusMatch ? statusMatch[1].trim() : null,
210
+ decided: decidedMatch ? decidedMatch[1].trim() : null,
211
+ evidence,
212
+ body,
213
+ };
214
+ }
215
+
216
+ function evidenceKey(e) {
217
+ return `${e.kind}::${e.ref}`;
218
+ }
219
+
220
+ function renderStub({ subject, decided, evidence, body }) {
221
+ const lines = [];
222
+ lines.push("---");
223
+ lines.push("status: needs-decision");
224
+ lines.push(`subject: ${subject}`);
225
+ lines.push(`decided: ${decided}`);
226
+ lines.push("evidence:");
227
+ for (const e of evidence) {
228
+ lines.push(` - kind: ${e.kind}`);
229
+ lines.push(` ref: ${e.ref}`);
230
+ }
231
+ lines.push("---");
232
+ lines.push(body);
233
+ return lines.join("\n");
234
+ }
235
+
236
+ // FIX 2: whole-phrase lowercasing for Title Case terms, acronym/CamelCase-safe.
237
+ function lowerPhrase(s) {
238
+ return s
239
+ .split(/(\s+)/)
240
+ .map((tok) => (/^[A-Z][a-z]+$/.test(tok) ? tok.toLowerCase() : tok))
241
+ .join("");
242
+ }
243
+
244
+ function defaultStubBody(subject, adrFiles) {
245
+ const adrLinks = adrFiles.map((f) => `[${f}](../adr/${f})`).join(", ");
246
+ return `
247
+ # ${subject}
248
+
249
+ This subject has provenance in frozen ADR history (${adrLinks}) but no living
250
+ decision has been ratified yet under the topic-keyed decision registry
251
+ (\`context/contracts/decision-registry-contract.md\`). This stub records that the
252
+ subject is open and links the frozen ADR(s) as provenance; it is not a decision.
253
+
254
+ When a living decision is ratified for ${lowerPhrase(subject)}, update this
255
+ file's \`status\` to \`current\`, add rationale, and keep the \`adr\` evidence
256
+ links as provenance for the history that led here.
257
+ `.trimStart();
258
+ }
259
+
260
+ function todayIso() {
261
+ return new Date().toISOString().slice(0, 10);
262
+ }
263
+
264
+ function upsertStub(group) {
265
+ const adrFiles = group.adrFiles;
266
+ const desiredEvidence = [
267
+ ...adrFiles.map((f) => ({ kind: "adr", ref: `docs/adr/${f}` })),
268
+ ...(group.extraEvidence || []),
269
+ ];
270
+
271
+ const existing = readExistingStub(group.slug);
272
+ const decided = existing?.decided || todayIso();
273
+ const body = existing?.body || defaultStubBody(group.subject, adrFiles);
274
+
275
+ const mergedEvidence = [];
276
+ const seen = new Set();
277
+ const source = existing ? existing.evidence.concat(desiredEvidence) : desiredEvidence;
278
+ for (const e of source) {
279
+ const key = evidenceKey(e);
280
+ if (seen.has(key)) continue;
281
+ seen.add(key);
282
+ mergedEvidence.push(e);
283
+ }
284
+
285
+ const content = renderStub({ subject: group.subject, decided, evidence: mergedEvidence, body });
286
+
287
+ const full = path.join(DECISIONS_DIR, `${group.slug}.md`);
288
+ const priorRaw = fs.existsSync(full) ? fs.readFileSync(full, "utf8") : null;
289
+ if (priorRaw === content) {
290
+ return { slug: group.slug, changed: false };
291
+ }
292
+ fs.mkdirSync(DECISIONS_DIR, { recursive: true });
293
+ fs.writeFileSync(full, content, "utf8");
294
+ return { slug: group.slug, changed: true };
295
+ }
296
+
297
+ // Fail loudly if any numbered ADR file is unassigned or double-assigned, or a
298
+ // group references a missing file — silent drop of an ADR is exactly the bug a
299
+ // freeze must never ship.
300
+ function assertCoverage(adrFilenames) {
301
+ const assigned = new Map();
302
+ for (const g of SUBJECT_GROUPS) {
303
+ for (const f of g.adrFiles) {
304
+ if (assigned.has(f)) {
305
+ throw new Error(`ADR ${f} is assigned to two groups (${assigned.get(f)} and ${g.slug})`);
306
+ }
307
+ assigned.set(f, g.slug);
308
+ if (!adrFilenames.includes(f)) {
309
+ throw new Error(`group ${g.slug} references missing ADR file ${f}`);
310
+ }
311
+ }
312
+ }
313
+ const unassigned = adrFilenames.filter((f) => !assigned.has(f));
314
+ if (unassigned.length > 0) {
315
+ throw new Error(`ADR file(s) not assigned to any subject group: ${unassigned.join(", ")}`);
316
+ }
317
+ }
318
+
319
+ function main() {
320
+ const adrFilenames = listAdrFiles();
321
+ if (adrFilenames.length === 0) {
322
+ console.log("No docs/adr/NNNN-*.md files found; nothing to freeze.");
323
+ return 0;
324
+ }
325
+
326
+ assertCoverage(adrFilenames);
327
+
328
+ const frozen = adrFilenames.map(freezeOne);
329
+ const bannerChanged = frozen.filter((f) => f.changed);
330
+ for (const f of bannerChanged) {
331
+ console.log(`Froze ${path.relative(ROOT, path.join(ADR_DIR, f.file))} (banner inserted).`);
332
+ }
333
+
334
+ const meta = frozen.map((f) => parseAdrMeta(f.file, f.raw));
335
+ const adrIndexContent = renderAdrIndex(meta);
336
+ const priorIndex = fs.existsSync(ADR_INDEX_PATH) ? fs.readFileSync(ADR_INDEX_PATH, "utf8") : null;
337
+ if (priorIndex !== adrIndexContent) {
338
+ fs.writeFileSync(ADR_INDEX_PATH, adrIndexContent, "utf8");
339
+ console.log(`Wrote ${path.relative(ROOT, ADR_INDEX_PATH)}.`);
340
+ }
341
+
342
+ const stubResults = SUBJECT_GROUPS.map((g) => upsertStub(g));
343
+ for (const r of stubResults.filter((r) => r.changed)) {
344
+ console.log(`Wrote docs/decisions/${r.slug}.md (needs-decision stub).`);
345
+ }
346
+
347
+ // Chain the existing (not vendored) registry index generator so a single
348
+ // `npm run freeze:adrs` leaves docs/decisions/index.md current too.
349
+ const checkDecisions = path.join(ROOT, "scripts", "check-decisions.cjs");
350
+ if (fs.existsSync(checkDecisions)) {
351
+ execFileSync(process.execPath, [checkDecisions, "gen-index"], { cwd: ROOT, stdio: "inherit" });
352
+ }
353
+
354
+ const anyChange =
355
+ bannerChanged.length > 0 ||
356
+ priorIndex !== adrIndexContent ||
357
+ stubResults.some((r) => r.changed);
358
+ console.log(
359
+ anyChange ? "freeze-adrs: changes written." : "freeze-adrs: no-op (already frozen and current)."
360
+ );
361
+ return 0;
362
+ }
363
+
364
+ process.exit(main());
@@ -131,6 +131,18 @@ function checkProtectedPathPattern(filePath) {
131
131
  };
132
132
  }
133
133
 
134
+ // .kontourai/flow-agents/current/<actor>.json (#291) — the per-actor projection of the same
135
+ // pointer above. An agent could forge active_flow_id/active_step_id here exactly as it could
136
+ // via the legacy global file, so this is protected identically (same reason text).
137
+ // SAFE: the workflow CLI writes this via writePerActorCurrent → fs.writeFileSync,
138
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
139
+ if (/(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\/[^/]+\.json$/.test(norm)) {
140
+ return {
141
+ name: '.kontourai/flow-agents/current/<actor>.json',
142
+ reason: 'an agent could forge active_flow_id/active_step_id to route the gate to a permissive FlowDefinition',
143
+ };
144
+ }
145
+
134
146
  // .kontourai/flow-agents/.goal-fit-block-streak.json controls soft-block
135
147
  // release counting. An agent could force early advisory-gate release by
136
148
  // writing a high count.
@@ -403,12 +415,13 @@ function checkCommandForBypass(command) {
403
415
  * Regex that matches a normalized (forward-slash) path ending with a protected
404
416
  * kill-switch file: shell profiles, .claude/settings*.json,
405
417
  * .kontourai/flow-agents/current.json,
418
+ * .kontourai/flow-agents/current/<actor>.json (#291 per-actor projection),
406
419
  * .kontourai/flow-agents/.goal-fit-block-streak.json,
407
420
  * .kontourai/flow-agents/<slug>/state.json,
408
421
  * .kontourai/flow-agents/<slug>/trust.bundle, and deprecated runtime-shaped
409
422
  * .flow-agents equivalents.
410
423
  */
411
- const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
424
+ const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\/[^/]+\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
412
425
 
413
426
  /**
414
427
  * Return true when a token (an unquoted redirect target or tee argument) matches
@@ -49,6 +49,8 @@
49
49
  const fs = require('fs');
50
50
  const path = require('path');
51
51
  const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
52
+ const { resolveActor } = require('./lib/actor-identity.js');
53
+ const { readCurrentPointer } = require('./lib/current-pointer.js');
52
54
  const crypto = require('crypto');
53
55
 
54
56
  const MAX_STDIN = 1024 * 1024;
@@ -198,8 +200,9 @@ function latestStateDir(flowAgentsDir) {
198
200
  * to the newest-mtime state.json directory.
199
201
  */
200
202
  function resolveArtifactDir(root) {
203
+ const actorKey = resolveActor(process.env).actor;
201
204
  for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
202
- const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
205
+ const { payload: current } = readCurrentPointer(flowAgentsDir, actorKey);
203
206
  if (current) {
204
207
  const slug = current.artifact_dir || current.active_slug;
205
208
  if (typeof slug === 'string' && slug.trim()) {
@@ -22,6 +22,8 @@ const SANCTIONED_REMEDIES = {
22
22
  'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
23
23
  '.kontourai/flow-agents/current.json':
24
24
  'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
25
+ '.kontourai/flow-agents/current/<actor>.json':
26
+ 'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
25
27
  '.kontourai/flow-agents/.goal-fit-block-streak.json':
26
28
  'This file is only mutated internally by the goal-fit Stop hook; there is no sanctioned agent writer. Never disable this hook to make the write.',
27
29
  '.kontourai/flow-agents/<slug>/state.json':
@@ -61,6 +63,13 @@ const REMEDY_COMMAND_CANDIDATES = [
61
63
  { name: '.kontourai/flow-agents/<slug>/trust.bundle', needles: ['trust.bundle'] },
62
64
  { name: '.kontourai/flow-agents/<slug>/state.json', needles: ['state.json'] },
63
65
  { name: '.kontourai/flow-agents/.goal-fit-block-streak.json', needles: ['.goal-fit-block-streak.json'] },
66
+ // #291: the per-actor projection (current/<actor>.json) is checked BEFORE the legacy
67
+ // current.json needle below — its needle is a directory-scoped substring
68
+ // ('flow-agents/current/') that never overlaps with the bare 'current.json' basename, so
69
+ // ordering between these two entries has no effect on real inputs, but keeping the more
70
+ // specific per-actor entry first mirrors this table's existing "specific before generic"
71
+ // convention.
72
+ { name: '.kontourai/flow-agents/current/<actor>.json', needles: ['flow-agents/current/', '.flow-agents/current/'] },
64
73
  { name: '.kontourai/flow-agents/current.json', needles: ['current.json'] },
65
74
  { name: '.claude/settings.local.json', needles: ['settings.local.json'] },
66
75
  { name: '.claude/settings.json', needles: ['.claude/settings.json'] },
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ /**
3
+ * current-pointer.js — shared pure-CJS per-actor "current" pointer reader/writer (#291)
4
+ *
5
+ * Zero external dependencies (only Node core: fs, path). Consumed by:
6
+ * - build/src/cli/workflow-sidecar.js (ESM compiled, via createRequire — Wave 2 Task 2.1)
7
+ * - build/src/lib/flow-resolver.js (ESM compiled, via createRequire — Wave 2 Task 2.2)
8
+ * - scripts/hooks/stop-goal-fit.js, evidence-capture.js, lib/liveness-heartbeat.js,
9
+ * scripts/statusline/flow-agents-statusline.js (CJS, direct require — Wave 2 Task 2.3)
10
+ * - scripts/hooks/workflow-steering.js (CJS, direct require — Wave 2 Task 2.4)
11
+ *
12
+ * Purpose (issue #291): today every one of the consumers above hand-rolls its own
13
+ * `fs.readFileSync(path.join(flowAgentsDir, "current.json"))`, so once `ensure-session` starts
14
+ * projecting a per-actor `current/<actor>.json` pointer (Wave 2 Task 2.1's `writeCurrent()` dual
15
+ * write), every reader must gain the SAME actor-aware preference or session A's own "what am I
16
+ * working on" view keeps getting silently overwritten by session B's more-recent legacy write.
17
+ * This module is the single choke point for that preference rule — every reader listed above
18
+ * calls `readCurrentPointer()` instead of hand-rolling its own fallback, so the compat-shim rule
19
+ * (per-actor first, legacy-global fallback) can never drift between call sites.
20
+ *
21
+ * `sanitizeSegment` is required from `./actor-identity.js` (already reviewed: restricts to
22
+ * `[A-Za-z0-9_.-]`, caps 64 chars) — this file deliberately does NOT re-implement a second
23
+ * sanitizer; it is the same charset restriction `assignment-provider.ts`'s `assignmentFilePath()`
24
+ * already applies to its own per-subject filenames, so the naming convention is consistent across
25
+ * both per-key-file stores in this repo (`assignment/<subject>.json` and now
26
+ * `current/<actor>.json`).
27
+ *
28
+ * Exports:
29
+ * perActorCurrentFile(flowAgentsDir, actorKey) → string (path, unsanitized existence not
30
+ * implied — caller must still fs.existsSync)
31
+ * readCurrentPointer(flowAgentsDir, actorKey) → { payload: object|null,
32
+ * source: "per-actor"|"legacy"|"none",
33
+ * file: string|null }
34
+ * writePerActorCurrent(flowAgentsDir, actorKey, payload) → void
35
+ */
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const { sanitizeSegment, isUnresolvedActor } = require('./actor-identity.js');
40
+
41
+ /**
42
+ * Best-effort tolerant JSON read: missing file or corrupt/unparseable content are BOTH treated
43
+ * as "absent" (returns null), never thrown — this is an advisory read used to decide which
44
+ * pointer file to prefer, not a durable-write persistence path. Mirrors the `readJsonFile`
45
+ * tolerance convention already used by stop-goal-fit.js/evidence-capture.js for this exact file
46
+ * (`current.json`) today.
47
+ *
48
+ * @param {string} file
49
+ * @returns {object|null}
50
+ */
51
+ function readJsonFileTolerant(file) {
52
+ try {
53
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Path to the per-actor current pointer file for a given actor key, sanitized via the shared
61
+ * `sanitizeSegment` (reused, not re-implemented) so the filename is path-traversal-safe exactly
62
+ * like `assignment-provider.ts`'s `assignmentFilePath()`.
63
+ *
64
+ * @param {string} flowAgentsDir
65
+ * @param {string} actorKey
66
+ * @returns {string}
67
+ */
68
+ function perActorCurrentFile(flowAgentsDir, actorKey) {
69
+ return path.join(flowAgentsDir, 'current', `${sanitizeSegment(actorKey)}.json`);
70
+ }
71
+
72
+ /**
73
+ * The ENTIRE compat-shim fallback rule (#291): when `actorKey` is a non-empty, resolved (not
74
+ * `isUnresolvedActor`) string and its per-actor file exists and parses, prefer it
75
+ * (`source: "per-actor"`). Otherwise — no actorKey, an unresolved actor, or no per-actor file yet
76
+ * — fall back to the legacy global `<flowAgentsDir>/current.json`; if THAT exists and parses,
77
+ * return it (`source: "legacy"`). Otherwise return `{ payload: null, source: "none", file: null }`.
78
+ *
79
+ * A missing/corrupt per-actor file is tolerated as absent (falls through to the legacy branch,
80
+ * never throws) — same best-effort tolerance the existing `readJsonFile`-style readers already
81
+ * apply to `current.json` today. This fallback must be EXACTLY equivalent to today's plain
82
+ * `current.json` read for every caller that has no actorKey (or an unresolved one) — that is
83
+ * what makes this a compat shim rather than a behavior change.
84
+ *
85
+ * @param {string} flowAgentsDir
86
+ * @param {string} [actorKey]
87
+ * @returns {{ payload: object|null, source: "per-actor"|"legacy"|"none", file: string|null }}
88
+ */
89
+ function readCurrentPointer(flowAgentsDir, actorKey) {
90
+ const key = actorKey == null ? '' : String(actorKey);
91
+ if (key && !isUnresolvedActor(key)) {
92
+ const perActorFile = perActorCurrentFile(flowAgentsDir, key);
93
+ const perActorPayload = readJsonFileTolerant(perActorFile);
94
+ if (perActorPayload !== null) return { payload: perActorPayload, source: 'per-actor', file: perActorFile };
95
+ }
96
+
97
+ const legacyFile = path.join(flowAgentsDir, 'current.json');
98
+ const legacyPayload = readJsonFileTolerant(legacyFile);
99
+ if (legacyPayload !== null) return { payload: legacyPayload, source: 'legacy', file: legacyFile };
100
+
101
+ return { payload: null, source: 'none', file: null };
102
+ }
103
+
104
+ /**
105
+ * Durable write-side counterpart to `readCurrentPointer`'s per-actor branch — used only by
106
+ * `workflow-sidecar.ts`'s `writeCurrent()` (Wave 2 Task 2.1), which calls this ALONGSIDE
107
+ * (never instead of) its existing unconditional legacy `current.json` write, so the legacy
108
+ * compat-shim fallback always has a value to read for any caller that never resolves an actor.
109
+ * `fs.mkdirSync` the `current/` subdir with `{ recursive: true }` before writing — this is the
110
+ * ONLY writer of that subdir, so no other caller needs to pre-create it.
111
+ *
112
+ * @param {string} flowAgentsDir
113
+ * @param {string} actorKey
114
+ * @param {object} payload
115
+ * @returns {void}
116
+ */
117
+ function writePerActorCurrent(flowAgentsDir, actorKey, payload) {
118
+ const file = perActorCurrentFile(flowAgentsDir, actorKey);
119
+ fs.mkdirSync(path.dirname(file), { recursive: true });
120
+ fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`);
121
+ }
122
+
123
+ module.exports = { perActorCurrentFile, readCurrentPointer, writePerActorCurrent };