@llblab/pi-kit 0.18.2 → 0.19.1

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 (113) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +23 -1
  6. package/node_modules/@llblab/pi-state-flow/README.md +102 -48
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +160 -255
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
  60. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
  61. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
  62. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
  63. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
  64. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
  65. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
  66. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
  67. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
  68. package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
  69. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
  70. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
  71. package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
  72. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
  73. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
  74. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
  75. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
  76. package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
  77. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
  79. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
  80. package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
  81. package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
  82. package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
  83. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
  84. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
  85. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
  86. package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
  88. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +155 -254
  90. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
  91. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
  92. package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
  93. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
  94. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
  95. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
  96. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
  97. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
  98. package/node_modules/@llblab/pi-state-flow/package.json +9 -6
  99. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
  100. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
  101. package/package.json +2 -2
  102. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
  103. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
  104. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
  105. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
  106. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
  107. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
  108. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
  109. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
  110. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
  111. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
  112. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
  113. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
@@ -1,6 +1,5 @@
1
1
  // Domain: exact file-cohort publication, current-only recovery, and cooperating worktree exclusion.
2
2
  // Excludes: temporal algebra, Pi lifecycle, Git objects/remotes, and backend fallback policy.
3
- import { spawnSync } from "node:child_process";
4
3
  import { createHash } from "node:crypto";
5
4
  import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
5
  import { dirname, relative, resolve } from "node:path";
@@ -9,10 +8,10 @@ import {
9
8
  serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates,
10
9
  type DurableFileBase, type OwnedFileUpdate,
11
10
  } from "./durable.ts";
12
- import { parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
11
+ import type { ArtifactProvenanceRegistry } from "./artifact.ts";
12
+ import { MAX_HISTORY_LIMIT } from "./history.ts";
13
13
  import { hashJson, sameJson } from "./json.ts";
14
14
  import { RevisionUnavailableError, isFileRevision, parseSessionRuntime, serializeSessionRuntime, type FileRevision, type SessionRuntime } from "./snapshot.ts";
15
- import { planLegacyStorageMigration } from "./migration.ts";
16
15
  export { isFileRevision, type FileRevision } from "./snapshot.ts";
17
16
  import type { StateScope } from "./state.ts";
18
17
  import { validateTemporalState, type TemporalState } from "./temporal.ts";
@@ -20,16 +19,6 @@ import { validateTemporalState, type TemporalState } from "./temporal.ts";
20
19
  const SCOPES = ["global", "cwd", "session"] as const;
21
20
  export interface TemporalFileBase { files: DurableFileBase[] }
22
21
 
23
- /** Probe once at a lifecycle boundary, never on cached state reads. Only spawn ENOENT is absence. */
24
- export function detectGitCapability(): "git" | "files" {
25
- const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 15_000 });
26
- if (result.error && (result.error as NodeJS.ErrnoException).code === "ENOENT") return "files";
27
- if (result.error || result.status !== 0) {
28
- throw new RevisionUnavailableError(`Cannot resolve Git capability: ${result.error?.message ?? result.stderr ?? `exit ${result.status}`}`);
29
- }
30
- return "git";
31
- }
32
-
33
22
  export function assertStorageDirectory(path: string): void {
34
23
  const root = resolve(path);
35
24
  const parent = dirname(root);
@@ -72,7 +61,7 @@ export function acquirePublicationLock(path: string, unavailable: (cause: unknow
72
61
  }
73
62
  }
74
63
 
75
- /** Git writers also acquire this lock before their common-Git-directory lock. */
64
+ /** Canonical writers and bounded backup capture share exclusion; no Git work runs under this lock. */
76
65
  export function withStoragePublicationLock<T>(repositoryRoot: string, action: (root: string) => T): T {
77
66
  const root = resolve(repositoryRoot);
78
67
  assertStorageDirectory(root);
@@ -96,7 +85,7 @@ export function assertTemporalFileBase(expected: TemporalFileBase, current: Temp
96
85
  })) throw new Error("Temporal State Flow base or scope identity changed concurrently");
97
86
  }
98
87
 
99
- /** One shared publication plan for Git and files; neither backend invents semantic changes. */
88
+ /** Plan exact canonical updates; lifecycle-only writes exclude semantic files and provenance. */
100
89
  export function planTemporalPublication(
101
90
  cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[],
102
91
  current: TemporalFileBase, root: string, runtime?: SessionRuntime, runtimeOnly = false, sessionKey = sessionId,
@@ -104,11 +93,10 @@ export function planTemporalPublication(
104
93
  ): { updates: OwnedFileUpdate[]; changedScopes: StateScope[] } {
105
94
  const candidates = temporalStateFileUpdates(cwd, sessionId, view, scopes, root, sessionKey);
106
95
  const files = new Map(current.files.map((file) => [file.path, file]));
107
- if (runtimeOnly && scopes.length !== 0) throw new Error("Runtime-only publication cannot write semantic scopes");
96
+ if (runtimeOnly && (scopes.length !== 0 || provenance !== undefined)) throw new Error("Runtime-only publication cannot write semantic scopes or artifact provenance");
108
97
  const changedScopes: StateScope[] = [];
109
98
  for (const scope of SCOPES) {
110
99
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
111
- if (files.get(resolve(paths.directory, "state.json"))!.identity !== "missing") throw new Error("Legacy State Flow storage requires explicit migration");
112
100
  const previous = parseScopeStream(files.get(paths.checkpoint)!.content, files.get(paths.patches)!.content, scope,
113
101
  scope === "cwd" ? cwd : undefined, files.get(paths.meta)!.content);
114
102
  if (runtimeOnly || (previous !== undefined && sameJson(previous, view.scopes[scope]))) continue;
@@ -129,7 +117,7 @@ export function planTemporalPublication(
129
117
  }
130
118
  }
131
119
  const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
132
- const previousRuntime = parseSessionRuntime(files.get(runtimePaths.config)!.content, files.get(runtimePaths.runtime)!.content, cwd, sessionId, files.get(runtimePaths.meta)!.content);
120
+ const previousRuntime = parseSessionRuntime(files.get(runtimePaths.config)!.content, files.get(runtimePaths.runtime)!.content, cwd, sessionId);
133
121
  if (previousRuntime !== undefined && changedScopes.length > 0 && runtime === undefined) throw new Error("Temporal semantic publication requires its session runtime cohort");
134
122
  const runtimeUpdates: OwnedFileUpdate[] = [];
135
123
  if (runtime !== undefined) {
@@ -138,12 +126,6 @@ export function planTemporalPublication(
138
126
  if (files.get(runtimePaths.config)!.content !== sources.config || files.get(runtimePaths.runtime)!.content !== sources.runtime) {
139
127
  runtimeUpdates.push({ path: runtimePaths.config, content: sources.config }, { path: runtimePaths.runtime, content: sources.runtime });
140
128
  }
141
- if (files.get(runtimePaths.runtime)!.identity === "missing" && files.get(runtimePaths.meta)!.content !== undefined
142
- && previousRuntime !== undefined && !provenanceUpdates.some(({ path }) => path === runtimePaths.meta)) {
143
- const registry = provenance?.session ?? parseArtifactProvenanceRegistry(previousRuntime.meta.artifacts, "State Flow session artifact provenance");
144
- const content = serializeScopeMetadata(registry, view.scopes.session, "session", undefined, files.get(runtimePaths.meta)!.content);
145
- runtimeUpdates.push({ path: runtimePaths.meta, content });
146
- }
147
129
  }
148
130
  const changedPaths = new Set(changedScopes.flatMap((scope) => {
149
131
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
@@ -174,17 +156,16 @@ function decodeFileCohort(cwd: string, sessionId: string, root: string, base: Te
174
156
  const scopes = {} as TemporalState["scopes"];
175
157
  for (const scope of SCOPES) {
176
158
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
177
- if (files.get(resolve(paths.directory, "state.json")) !== undefined) throw new Error("Legacy State Flow storage requires explicit migration");
178
159
  const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope,
179
160
  scope === "cwd" ? cwd : undefined, files.get(paths.meta));
180
161
  if (!stream) throw new Error("Incomplete file-only temporal scope cohort");
181
162
  scopes[scope] = stream;
182
163
  }
183
164
  const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
184
- const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), cwd, sessionId, files.get(paths.meta));
185
- if (!runtime || runtime.meta.publication !== "files") throw new Error("File-only recovery requires file publication provenance, not a Git self reference");
165
+ const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), cwd, sessionId);
166
+ if (!runtime) throw new Error("Canonical recovery requires a complete session runtime");
186
167
  const view = { scopes, lineage: runtime.meta.lineage };
187
- validateTemporalState(view);
168
+ validateTemporalState(view, MAX_HISTORY_LIMIT);
188
169
  const provenance: Record<StateScope, ArtifactProvenanceRegistry> = {
189
170
  global: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta),
190
171
  cwd: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta),
@@ -207,17 +188,16 @@ export function loadTemporalFileRevision(cwd: string, sessionId: string, root: s
207
188
  });
208
189
  }
209
190
 
210
- /** Publish a validated full runtime/scoped cohort; no Git commands, success receipts, or pending pushes. */
191
+ /** Publish a validated canonical cohort; runtimeOnly owns only session config/runtime files. */
211
192
  export function publishTemporalStateToFiles(
212
193
  cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[],
213
194
  base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey = sessionId,
214
- provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>,
195
+ provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>, runtimeOnly = false,
215
196
  ): { base: TemporalFileBase; revision: FileRevision; changed: boolean } {
216
197
  return withStoragePublicationLock(root, (locked) => {
217
- if (runtime.meta.publication !== "files") throw new Error("File publication requires explicit file provenance");
218
198
  const current = { files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) };
219
199
  assertTemporalFileBase(base, current);
220
- const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, locked, runtime, false, sessionKey, provenance);
200
+ const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, locked, runtime, runtimeOnly, sessionKey, provenance);
221
201
  const next = { files: temporalFileReceipts(current, updates) };
222
202
  decodeFileCohort(cwd, sessionId, locked, next, sessionKey);
223
203
  const revision = fileRevision(next, locked);
@@ -240,11 +220,3 @@ function publishFileUpdates(bases: readonly DurableFileBase[], updates: readonly
240
220
  throw error;
241
221
  }
242
222
  }
243
-
244
- /** In-store format conversion only; no Git history or cross-repository import. */
245
- export function migrateLegacyStorageToFiles(cwd: string, sessionId: string, root: string, sessionKey = sessionId): void {
246
- withStoragePublicationLock(root, (locked) => {
247
- const plan = planLegacyStorageMigration(cwd, sessionId, locked, undefined, sessionKey);
248
- if (plan.updates.length) publishFileUpdates(plan.bases, plan.updates, locked);
249
- });
250
- }
@@ -192,7 +192,7 @@ function renderStateFlowTelegramField(value: unknown): string {
192
192
  }
193
193
 
194
194
  export function renderStateFlowRichState(scope: StateFlowTelegramScope, step: number, state: StateFlowTelegramState): StateFlowTelegramRichMessage {
195
- const fields = ["artifacts", "contract", "working", "intents", "response", "lazy"] as const;
195
+ const fields = ["intents", "contract", "working", "artifacts", "response", "lazy"] as const;
196
196
  return {
197
197
  blocks: [
198
198
  {
@@ -1,4 +1,4 @@
1
- import { RECENT_TRANSITION_LIMIT, validateRecentTransition, type RecentScopePatch } from "./history.ts";
1
+ import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, validateRecentTransition, type RecentScopePatch } from "./history.ts";
2
2
  import { applyPatch, containsNull, isJsonValue, isObject, sameJson, type JsonObject } from "./json.ts";
3
3
  import { isMaterializedState, overlayStates, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
4
4
 
@@ -33,6 +33,12 @@ export interface TemporalState {
33
33
 
34
34
  const SCOPES: StateScope[] = ["global", "cwd", "session"];
35
35
 
36
+ function validateHistoryLimit(limit: number): void {
37
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_HISTORY_LIMIT) {
38
+ throw new Error(`State Flow history limit must be an integer from 0 to ${MAX_HISTORY_LIMIT}`);
39
+ }
40
+ }
41
+
36
42
  function validateBoundary(boundary: TransitionBoundary): void {
37
43
  if (!isObject(boundary) || Object.keys(boundary).sort().join(",") !== "id,parent,position"
38
44
  || typeof boundary.id !== "string" || boundary.id.trim().length === 0
@@ -60,7 +66,8 @@ function sameBoundary(left: TransitionBoundary, right: TransitionBoundary): bool
60
66
  }
61
67
 
62
68
  /** Replay validation is shared by disk codecs and active-lineage materialization. */
63
- export function validateScopeStream(value: unknown, scope: StateScope): asserts value is ScopeStream {
69
+ export function validateScopeStream(value: unknown, scope: StateScope, historyLimit = DEFAULT_HISTORY_LIMIT): asserts value is ScopeStream {
70
+ validateHistoryLimit(historyLimit);
64
71
  if (!SCOPES.includes(scope)) throw new Error("Unknown temporal scope");
65
72
  if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "checkpoint,patches"
66
73
  || !isObject(value.checkpoint) || Object.keys(value.checkpoint).sort().join(",") !== "state,through"
@@ -70,7 +77,7 @@ export function validateScopeStream(value: unknown, scope: StateScope): asserts
70
77
  const stream = value as unknown as ScopeStream;
71
78
  validateBoundary(stream.checkpoint.through);
72
79
  validateState(stream.checkpoint.state);
73
- if (stream.patches.length > RECENT_TRANSITION_LIMIT) throw new Error("Temporal scope tail exceeds seven patches");
80
+ if (stream.patches.length > historyLimit) throw new Error(`Temporal scope tail exceeds configured history limit ${historyLimit}`);
74
81
  let previous = stream.checkpoint.through;
75
82
  let state = stream.checkpoint.state;
76
83
  const identities = new Set([previous.id]);
@@ -94,9 +101,10 @@ export function validateScopeStream(value: unknown, scope: StateScope): asserts
94
101
  }
95
102
  }
96
103
 
97
- export function validateTemporalLineage(value: unknown): asserts value is TransitionBoundary[] {
98
- if (!isJsonValue(value) || !Array.isArray(value) || value.length === 0 || value.length > RECENT_TRANSITION_LIMIT + 1) {
99
- throw new Error("Temporal lineage must contain between one and eight boundaries");
104
+ export function validateTemporalLineage(value: unknown, historyLimit = DEFAULT_HISTORY_LIMIT): asserts value is TransitionBoundary[] {
105
+ validateHistoryLimit(historyLimit);
106
+ if (!isJsonValue(value) || !Array.isArray(value) || value.length === 0 || value.length > historyLimit + 1) {
107
+ throw new Error(`Temporal lineage must contain between one and ${historyLimit + 1} boundaries`);
100
108
  }
101
109
  const seen = new Set<string>();
102
110
  for (let index = 0; index < value.length; index++) {
@@ -111,9 +119,28 @@ export function validateTemporalLineage(value: unknown): asserts value is Transi
111
119
  }
112
120
  }
113
121
 
122
+ /** Bind one owned stream to its runtime lineage without requiring patches from unrelated scopes. */
123
+ export function validateScopeLineage(stream: ScopeStream, scope: StateScope, lineage: readonly TransitionBoundary[], historyLimit = DEFAULT_HISTORY_LIMIT): void {
124
+ validateTemporalLineage(lineage, historyLimit);
125
+ validateScopeStream(stream, scope, historyLimit);
126
+ const oldest = lineage[0]!;
127
+ const head = lineage.at(-1)!;
128
+ if (stream.checkpoint.through.position > oldest.position) throw new Error("Scope checkpoint is newer than the guaranteed hot boundary");
129
+ const identities = new Map(lineage.map((boundary) => [boundary.id, boundary]));
130
+ for (const boundary of [stream.checkpoint.through, ...stream.patches.map((record) => record.transition)]) {
131
+ if (boundary.position > head.position) throw new Error("Temporal scope patch is beyond the active head");
132
+ const identity = identities.get(boundary.id);
133
+ const position = lineage[boundary.position - oldest.position];
134
+ if ((identity && !sameBoundary(identity, boundary)) || (position && !sameBoundary(position, boundary))) {
135
+ throw new Error("Conflicting State Flow temporal lineage");
136
+ }
137
+ }
138
+ }
139
+
114
140
  /** Validate one revision-selected cohort. Its older ancestry must be bound by the durable loader. */
115
- export function validateTemporalState(view: TemporalState): void {
116
- validateTemporalLineage(view.lineage);
141
+ export function validateTemporalState(view: TemporalState, historyLimit = DEFAULT_HISTORY_LIMIT): void {
142
+ validateHistoryLimit(historyLimit);
143
+ validateTemporalLineage(view.lineage, historyLimit);
117
144
  const oldest = view.lineage[0]!;
118
145
  const identities = new Map<string, TransitionBoundary>();
119
146
  const positions = new Map<number, TransitionBoundary>();
@@ -131,7 +158,7 @@ export function validateTemporalState(view: TemporalState): void {
131
158
  const head = view.lineage.at(-1)!;
132
159
  for (const scope of SCOPES) {
133
160
  const stream = view.scopes[scope];
134
- validateScopeStream(stream, scope);
161
+ validateScopeStream(stream, scope, historyLimit);
135
162
  remember(stream.checkpoint.through);
136
163
  if (stream.checkpoint.through.position > oldest.position) {
137
164
  throw new Error("Scope checkpoint is newer than the guaranteed hot boundary");
@@ -153,23 +180,22 @@ export function validateTemporalState(view: TemporalState): void {
153
180
  }
154
181
 
155
182
  /** Adopt revision-proven inherited streams without rewriting their checkpoints or tails. */
156
- export function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id: string): TemporalState {
183
+ export function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id: string, historyLimit = DEFAULT_HISTORY_LIMIT): TemporalState {
157
184
  const boundaries = Object.values(scopes).flatMap((stream) => [stream.checkpoint.through, ...stream.patches.map((record) => record.transition)]);
158
185
  const origin: TransitionBoundary = { id, position: Math.max(...boundaries.map((boundary) => boundary.position)) + 1, parent: null };
159
186
  const view = { lineage: [origin], scopes: structuredClone(scopes) };
160
- validateTemporalState(view);
161
- return view;
187
+ return constrainTemporalState(view, historyLimit);
162
188
  }
163
189
 
164
190
  /** New or migrated state starts at a proven current boundary, with no invented past. */
165
- export function createTemporalState(states: ScopedStates, id: string): TemporalState {
191
+ export function createTemporalState(states: ScopedStates, id: string, historyLimit = DEFAULT_HISTORY_LIMIT): TemporalState {
166
192
  const through: TransitionBoundary = { id, position: 0, parent: null };
167
193
  const stream = (scope: StateScope): ScopeStream => ({
168
194
  checkpoint: { through: structuredClone(through), state: structuredClone(states[scope]) },
169
195
  patches: [],
170
196
  });
171
197
  const view: TemporalState = { lineage: [through], scopes: { global: stream("global"), cwd: stream("cwd"), session: stream("session") } };
172
- validateTemporalState(view);
198
+ validateTemporalState(view, historyLimit);
173
199
  return view;
174
200
  }
175
201
 
@@ -182,13 +208,62 @@ function scopeAt(stream: ScopeStream, boundary: TransitionBoundary): Materialize
182
208
  return state;
183
209
  }
184
210
 
211
+ /** Fold retained tails to a lower configured limit without inventing history. */
212
+ export function constrainTemporalState(view: TemporalState, historyLimit: number): TemporalState {
213
+ validateHistoryLimit(historyLimit);
214
+ validateTemporalState(view, MAX_HISTORY_LIMIT);
215
+ const next = structuredClone(view);
216
+ for (const scope of SCOPES) {
217
+ const stream = next.scopes[scope];
218
+ while (stream.patches.length > historyLimit) {
219
+ const folded = stream.patches.shift()!;
220
+ stream.checkpoint = { through: folded.transition, state: apply(stream.checkpoint.state, folded.patch) };
221
+ }
222
+ }
223
+ next.lineage = next.lineage.slice(-(historyLimit + 1));
224
+ validateTemporalState(next, historyLimit);
225
+ return next;
226
+ }
227
+
228
+ /** Select one scope at a proven retained boundary from its owning runtime lineage. */
229
+ export function selectScopeStreamAtBoundary(stream: ScopeStream, scope: StateScope, boundary: TransitionBoundary, historyLimit = DEFAULT_HISTORY_LIMIT): ScopeStream {
230
+ validateHistoryLimit(historyLimit);
231
+ validateBoundary(boundary);
232
+ validateScopeStream(stream, scope, historyLimit);
233
+ if (boundary.position < stream.checkpoint.through.position) {
234
+ throw new Error("Selected State Flow history boundary predates the retained scope checkpoint");
235
+ }
236
+ const selected = structuredClone(stream);
237
+ selected.patches = selected.patches.filter(({ transition }) => transition.position <= boundary.position);
238
+ validateScopeStream(selected, scope, historyLimit);
239
+ return selected;
240
+ }
241
+
242
+ /** Select one still-retained causal boundary without consulting an external history store. */
243
+ export function selectTemporalStateBoundary(view: TemporalState, boundaryId: string, historyLimit = DEFAULT_HISTORY_LIMIT): TemporalState {
244
+ validateHistoryLimit(historyLimit);
245
+ validateTemporalState(view, historyLimit);
246
+ if (typeof boundaryId !== "string" || boundaryId.trim().length === 0) throw new Error("State Flow temporal boundary identity must be non-empty");
247
+ const index = view.lineage.findIndex(({ id }) => id === boundaryId);
248
+ if (index < 0) throw new Error("Selected State Flow history boundary is outside the retained temporal window");
249
+ const target = view.lineage[index]!;
250
+ const selected = structuredClone(view);
251
+ selected.lineage = selected.lineage.slice(0, index + 1);
252
+ for (const scope of SCOPES) {
253
+ selected.scopes[scope].patches = selected.scopes[scope].patches.filter(({ transition }) => transition.position <= target.position);
254
+ }
255
+ validateTemporalState(selected, historyLimit);
256
+ return selected;
257
+ }
258
+
185
259
  /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
186
- export function readTemporalState(view: TemporalState, offset = 0, scope?: StateScope): MaterializedState {
187
- if (!Number.isSafeInteger(offset) || offset < 0 || offset > RECENT_TRANSITION_LIMIT) {
188
- throw new Error("State Flow hot-history offset must be an integer from 0 to 7");
260
+ export function readTemporalState(view: TemporalState, offset = 0, scope?: StateScope, historyLimit = DEFAULT_HISTORY_LIMIT): MaterializedState {
261
+ validateHistoryLimit(historyLimit);
262
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > historyLimit) {
263
+ throw new Error(`State Flow hot-history offset must be an integer from 0 to ${historyLimit}`);
189
264
  }
190
265
  if (scope !== undefined && !SCOPES.includes(scope)) throw new Error("Unknown temporal scope");
191
- validateTemporalState(view);
266
+ validateTemporalState(view, historyLimit);
192
267
  const boundary = view.lineage[view.lineage.length - 1 - offset];
193
268
  if (!boundary) throw new Error("Requested history predates the proven temporal origin");
194
269
  if (scope !== undefined) return scopeAt(view.scopes[scope], boundary);
@@ -200,8 +275,10 @@ export function advanceTemporalState(
200
275
  view: TemporalState,
201
276
  transitions: readonly RecentScopePatch[],
202
277
  id: string,
278
+ historyLimit = DEFAULT_HISTORY_LIMIT,
203
279
  ): TemporalState {
204
- validateTemporalState(view);
280
+ validateHistoryLimit(historyLimit);
281
+ validateTemporalState(view, historyLimit);
205
282
  if (transitions.length === 0) return view;
206
283
  validateRecentTransition({ id, at: 0, transitions });
207
284
  const head = view.lineage.at(-1)!;
@@ -221,13 +298,18 @@ export function advanceTemporalState(
221
298
  const next = structuredClone(view);
222
299
  for (const { scope, patch } of changes) {
223
300
  const stream = next.scopes[scope];
224
- if (stream.patches.length === RECENT_TRANSITION_LIMIT) {
301
+ if (historyLimit === 0) {
302
+ stream.checkpoint = { through: structuredClone(boundary), state: apply(scopeAt(stream, head), patch) };
303
+ stream.patches = [];
304
+ continue;
305
+ }
306
+ while (stream.patches.length >= historyLimit) {
225
307
  const folded = stream.patches.shift()!;
226
308
  stream.checkpoint = { through: folded.transition, state: apply(stream.checkpoint.state, folded.patch) };
227
309
  }
228
310
  stream.patches.push({ transition: structuredClone(boundary), patch: structuredClone(patch) });
229
311
  }
230
- next.lineage = [...next.lineage, boundary].slice(-(RECENT_TRANSITION_LIMIT + 1));
231
- validateTemporalState(next);
312
+ next.lineage = [...next.lineage, boundary].slice(-(historyLimit + 1));
313
+ validateTemporalState(next, historyLimit);
232
314
  return next;
233
315
  }
@@ -35,7 +35,7 @@ export interface StagedScopedTransition {
35
35
  }
36
36
 
37
37
  const SCOPES = new Set<StateScope>(["global", "cwd", "session"]);
38
- const PATCH_KEYS = new Set(["artifacts", "contract", "working", "intents", "lazy"]);
38
+ const PATCH_KEYS = new Set(["intents", "contract", "working", "artifacts", "lazy"]);
39
39
 
40
40
  function compileReadArtifacts(
41
41
  nextState: StateDocument,
@@ -46,10 +46,10 @@ function compileReadArtifacts(
46
46
  for (const read of successfulArtifactReads) {
47
47
  const output = patch.artifacts[read.path];
48
48
  if (!isObject(output)) {
49
- throw new Error(`Every successfully read invalidated artifact must have a global compiler output at artifacts[exact candidate path]; missing: ${read.path}`);
49
+ throw new Error(`Successfully read invalidated artifact requires compiler output at ${read.scope ?? "global"}.artifacts[${JSON.stringify(read.path)}]`);
50
50
  }
51
51
  const compiled = compileArtifact({
52
- source: { path: read.path, hash: read.hash },
52
+ source: { path: read.path, scope: read.scope, hash: read.hash, sourceFingerprint: read.sourceFingerprint },
53
53
  compiler: ORDINARY_ARTIFACT_COMPILER,
54
54
  output: output as ArtifactCompilerOutput,
55
55
  });
@@ -123,7 +123,7 @@ function validateScopePatch(scope: unknown, patch: unknown): asserts patch is Sc
123
123
  validatePatch(patch);
124
124
  for (const key of Object.keys(patch)) {
125
125
  if (!PATCH_KEYS.has(key)) {
126
- throw new Error(`Scoped State Flow patches cannot modify ${key}; only artifacts, contract, working, intents, and lazy are model-owned`);
126
+ throw new Error(`Unknown State Flow patch key ${JSON.stringify(key)}; expected one of: ${[...PATCH_KEYS].join(", ")}`);
127
127
  }
128
128
  }
129
129
  for (const key of ["artifacts", "contract", "working", "intents"] as const) {
@@ -131,8 +131,8 @@ function validateScopePatch(scope: unknown, patch: unknown): asserts patch is Sc
131
131
  throw new Error(`Scoped State Flow patch field ${key} must be a JSON object`);
132
132
  }
133
133
  }
134
- if (Object.hasOwn(patch, "lazy") && patch.lazy === null) {
135
- throw new Error("Scoped State Flow patch field lazy cannot be null");
134
+ if (Object.hasOwn(patch, "lazy") && !isObject(patch.lazy)) {
135
+ throw new Error("Scoped State Flow patch field lazy must be a JSON object");
136
136
  }
137
137
  if (isObject(patch.artifacts)) validateModelArtifactPatch(patch.artifacts);
138
138
  }
@@ -144,7 +144,7 @@ function completePatch(patch: ScopePatch, response: string): StatePatch {
144
144
  working: patch.working ?? {},
145
145
  intents: patch.intents ?? {},
146
146
  response,
147
- ...(Object.hasOwn(patch, "lazy") ? { lazy: structuredClone(patch.lazy!) } : {}),
147
+ lazy: structuredClone(patch.lazy ?? {}),
148
148
  };
149
149
  }
150
150
 
@@ -172,7 +172,8 @@ function stageScopedSemanticTransition(
172
172
  }
173
173
 
174
174
  const cwdPatch = patches.get("cwd") ?? {};
175
- const nextStates = structuredClone(currentStates);
175
+ const artifactReads = [...successfulArtifactReads];
176
+ const nextStates = { ...currentStates };
176
177
  const provenanceUpdates: Record<StateScope, Record<string, ArtifactProvenance>> = { global: {}, cwd: {}, session: {} };
177
178
  for (const scope of SCOPES) {
178
179
  const authored = patches.get(scope) ?? {};
@@ -180,8 +181,13 @@ function stageScopedSemanticTransition(
180
181
  ? acceptedResponse
181
182
  : currentStates[scope].response;
182
183
  const patch = completePatch(authored, response);
183
- const nextState = applyPatch(structuredClone(currentStates[scope]), patch) as MaterializedState;
184
- compileReadArtifacts(nextState, { artifacts: scope === "global" ? authored.artifacts ?? {} : {} }, scope === "global" ? successfulArtifactReads : [], provenanceUpdates.global);
184
+ const nextState = applyPatch(currentStates[scope], patch) as MaterializedState;
185
+ compileReadArtifacts(
186
+ nextState,
187
+ { artifacts: authored.artifacts ?? {} },
188
+ artifactReads.filter((read) => (read.scope ?? "global") === scope),
189
+ provenanceUpdates[scope],
190
+ );
185
191
  compileReadSkills(nextState, { artifacts: scope === "cwd" ? cwdPatch.artifacts ?? {} : {} }, scope === "cwd" ? successfulSkillReads : [], provenanceUpdates.cwd);
186
192
  validateMaterializedTransition(nextState);
187
193
  nextStates[scope] = nextState;
@@ -199,22 +205,6 @@ function stageScopedSemanticTransition(
199
205
  };
200
206
  }
201
207
 
202
- /** Validate that final eligibility has no pending acquisition/compilation obligation. */
203
- export function validateFinalEligibility(
204
- currentStates: ScopedStates,
205
- successfulSkillReads: Iterable<SuccessfulSkillRead>,
206
- causalBasis: string,
207
- successfulArtifactReads: Iterable<SuccessfulArtifactRead> = [],
208
- ): void {
209
- stageScopedSemanticTransition(
210
- currentStates,
211
- { transitions: [] },
212
- successfulSkillReads,
213
- causalBasis,
214
- successfulArtifactReads,
215
- );
216
- }
217
-
218
208
  /** Stage one canonical atomic scope cohort without changing the finalized response. */
219
209
  export function stageAtomicScopePatches(
220
210
  currentStates: ScopedStates,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.16.3",
3
+ "version": "0.17.3",
4
4
  "private": false,
5
5
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
6
6
  "keywords": [
@@ -66,13 +66,16 @@
66
66
  "node": ">=22.19.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "@earendil-works/pi-agent-core": ">=0.84.4",
70
- "@earendil-works/pi-ai": ">=0.84.4",
71
- "@earendil-works/pi-coding-agent": ">=0.84.4",
72
- "@earendil-works/pi-tui": ">=0.84.4"
69
+ "@earendil-works/pi-agent-core": ">=0.87.0",
70
+ "@earendil-works/pi-ai": ">=0.87.0",
71
+ "@earendil-works/pi-coding-agent": ">=0.87.0",
72
+ "@earendil-works/pi-tui": ">=0.87.0"
73
73
  },
74
74
  "devDependencies": {
75
- "@earendil-works/pi-tui": "0.84.4",
75
+ "@earendil-works/pi-agent-core": "0.87.0",
76
+ "@earendil-works/pi-ai": "0.87.0",
77
+ "@earendil-works/pi-coding-agent": "0.87.0",
78
+ "@earendil-works/pi-tui": "0.87.0",
76
79
  "@types/node": "latest",
77
80
  "typescript": "latest"
78
81
  }
@@ -2,7 +2,7 @@
2
2
  name: state-flow-guide
3
3
  description: >
4
4
  Explain State Flow or resolve a concrete read, patch, inheritance,
5
- acquisition, finalization, or recovery problem. Use on request or for a
5
+ acquisition, completion, or recovery problem. Use on request or for a
6
6
  blocked non-routine operation; not before every tool call and not for
7
7
  memory audits or unsolicited cleanup.
8
8
  ---
@@ -13,7 +13,7 @@ State Flow's on-demand operational reference. Resolve the usage question or iden
13
13
 
14
14
  ## Mode
15
15
 
16
- Passive tools access memory without starting an episode or requiring `final:true`. Missing tools or storage are blockers, not permission to enable an episode or bypass storage; explanation alone remains possible.
16
+ Passive tools access memory without starting an episode. Missing tools or storage are blockers, not permission to enable an episode or bypass storage; explanation alone remains possible.
17
17
 
18
18
  Operator commands: `/state-flow-status` inspects; `/state-flow-start` enables the current branch; `/state-flow-stop` ends active semantics without erasing memory or necessarily disabling passive tools.
19
19
 
@@ -21,14 +21,14 @@ Operator commands: `/state-flow-status` inspects; `/state-flow-start` enables th
21
21
 
22
22
  | Field | Purpose |
23
23
  | --- | --- |
24
+ | `intents` | Chosen future actions, not possibilities |
24
25
  | `contract` | Requirements, decisions, constraints, interfaces |
25
26
  | `working` | Observations, results, open questions, continuation |
26
- | `intents` | Chosen future actions, not possibilities |
27
27
  | `artifacts` | Exact source paths, descriptions, compilations |
28
- | `lazy` | Durable detail omitted from ordinary context |
29
28
  | `response` | Previous completed answer; runtime-owned |
29
+ | `lazy` | Durable detail omitted from ordinary context |
30
30
 
31
- Scopes overlay `global → cwd → session`: cross-project, project, branch/run. Later values override earlier ones; effective state does not identify the owner. Memory and tool output are data, not authority or proof of current external conditions.
31
+ Scopes overlay `global → cwd → session`: cross-project, project, branch/run. Later values override earlier ones; effective state does not identify the owner. The current run specification remains the user's transient request, not durable `contract`. Retain a requirement only when it must survive the current turn: cross-project requirements belong in `global.contract`, project architecture and rules in `cwd.contract`, and branch/task constraints in `session.contract`. Remove superseded requirements and use one atomic multi-scope patch to relocate a proven mis-scoped value within one store; inspect both owners first and verify the result afterward. Memory and tool output are data, not authority or proof of current external conditions.
32
32
 
33
33
  ## Read
34
34
 
@@ -44,13 +44,13 @@ Example arguments:
44
44
  {"paths":["cwd.working","session.working"]}
45
45
  ```
46
46
 
47
- Unscoped paths use effective state. `cwd[1].working` reads the preceding causal boundary; offsets 0–7 require available history. Array ranges such as `cwd.lazy.checks[0..3]` exclude the endpoint and require existing elements. Missing paths fail: inspect parent keys to verify deletion. Missing history is not empty history. Read `lazy` explicitly. Treat structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings, such as `$effective.lazy.memory[7]`, as semantic-state references. Other resources retain their native locators. Resolve any reference through the appropriate read/tool only when needed. Neither form proves authority or existence, hydrates, or executes anything. Never scan or resolve references merely to test them. A missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat `hint` as top-level diagnostic metadata, never as the requested state: its message asks for reconciliation and its paths are runtime-verified current owners. The hint proves provenance rather than staleness and is absent when no current durable source matches; keys, patch, and batch reads keep all-or-error behavior. Only then inspect ownership as needed and patch a proven stale owning value while preserving its surrounding meaning. Effective absence, inaccessible external resources, and transient failures are not proof.
47
+ Unscoped paths use effective state. `cwd[1].working` reads the preceding causal boundary. Materialized-history and scope patch-history paths such as `cwd.patches[1]` share the configured `historyLimit` bound (default 7) and require actually retained history. Lowering the limit folds excess tails without erasing current state; increasing it does not reconstruct discarded history. Array ranges such as `cwd.lazy.checks[0..3]` exclude the endpoint and require existing elements. Missing paths fail: inspect parent keys to verify deletion. Missing history is not empty history. Read `lazy` explicitly. Treat structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings, such as `$effective.lazy.memory[7]`, as semantic-state references. Other resources retain their native locators. Resolve any reference through the appropriate read/tool only when needed. Neither form proves authority or existence, hydrates, or executes anything. Never scan or resolve references merely to test them. A missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat `hint` as top-level diagnostic metadata, never as the requested state: its message asks for reconciliation and its paths are runtime-verified current owners. The hint proves provenance rather than staleness and is absent when no current durable source matches; keys, patch, and batch reads keep all-or-error behavior. Only then inspect ownership as needed and patch a proven stale owning value while preserving its surrounding meaning. Effective absence, inaccessible external resources, and transient failures are not proof.
48
48
 
49
49
  ## Write
50
50
 
51
- Call `patch_state` alone per assistant response; await acceptance before dependent work. Supply `global`, `cwd`, `session`, and/or `final`; supplied scopes commit atomically. Omit unchanged scopes.
51
+ Call `patch_state` alone per assistant response; await acceptance before dependent work. Supply one or more of `global`, `cwd`, and `session`; supplied scopes commit atomically. Omit unchanged scopes.
52
52
 
53
- Semantic planes `artifacts`, `contract`, `working`, and `intents` are objects; `lazy` accepts JSON without stored nulls. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion is forbidden.
53
+ Semantic planes `intents`, `contract`, `working`, `artifacts`, and the required `lazy` root are objects; nested lazy values may contain ordinary JSON without stored nulls. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion is forbidden.
54
54
 
55
55
  Illustrative deletion, only for an actually completed intent and after satisfying pending acquisitions:
56
56
 
@@ -64,16 +64,10 @@ Never edit backing files, `response`, configuration, provenance, or runtime meta
64
64
 
65
65
  Read sources for gaps, exact-source/edit needs, invalidation, contradiction, or explicit requests; descriptions are not acquired content.
66
66
 
67
- In active mode, include all pending acquisitions in the next atomic patch. Ordinary artifacts need exact-path descriptions in `global.artifacts`; read Skills, including this one, need `cwd.artifacts` entries with description, `kind: "skill"`, and nonempty `compilation` objects. Leave provenance to runtime; do not repeat accepted compilations.
68
-
69
- Before an active iteration's answer, obtain an accepted `final:true`. With no pending semantic or compilation changes:
70
-
71
- ```json
72
- {"final":true}
73
- ```
67
+ In active mode, include all pending acquisitions in the next atomic patch. Compile each invalidated ordinary artifact at its exact path in the reported scope (`global`, `cwd`, or `session`); do not relocate it or invent a global copy. If ownership is unclear, inspect the scoped registry rather than defaulting to global. Choose the narrowest scope for new artifacts. Read Skills, including this one, require `cwd.artifacts` entries with description, `kind: "skill"`, and nonempty `compilation` objects. Leave fingerprints, Skill hashes, and other provenance to runtime; do not repeat accepted compilations.
74
68
 
75
- This permits a later answer without preventing further work. Passive turns need no such call. If fallback preserves an answer, resolve finalization without restating it.
69
+ Before answering, reconcile future-relevant semantic or compilation changes through one or more material scope patches. If current durable state remains correct, do not call `patch_state`; ordinary completion requires no finalization patch.
76
70
 
77
71
  ## Recover
78
72
 
79
- After rejection or interruption, inspect the cause and accepted state before retrying only the intended change. Preserve unresolved conflicts; never delete locks or reset storage to force success. Restored memory does not undo tool effects. Local acceptance is not remote publication: push failure does not justify replaying semantic writes. Report blockers and stop after the identified operation.
73
+ After rejection or interruption, inspect the cause and accepted state before retrying only the intended change. Preserve unresolved conflicts; never delete locks or reset storage to force success. Restored memory does not undo tool effects. Canonical acceptance is independent of optional settled-turn backup: backup failure does not justify replaying semantic writes. Report blockers and stop after the identified operation.
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: state-flow-memory
3
3
  description: >
4
- Curate State Flow memory on request or once at an active State Flow feature,
5
- release, project-phase, or version boundary. Reconcile stale knowledge,
6
- contradictions, commitments, continuation, and ownership. Not for routine
7
- turns, usage help, or background maintenance.
4
+ Curate State Flow memory only on explicit user request. Reconcile stale
5
+ knowledge, contradictions, commitments, continuation, and ownership.
6
+ Not for routine turns, automatic phase-boundary audits, usage help, or
7
+ background maintenance.
8
8
  ---
9
9
 
10
10
  # State Flow Memory
@@ -19,7 +19,7 @@ Follow the installed runtime contract. In active mode, satisfy all pending acqui
19
19
 
20
20
  ## Reconcile one bounded set
21
21
 
22
- 1. **Limit the review.** Address the request or completed phase. Use targeted reads for gaps, contradictions, ownership, or verification; do not rerun the project.
22
+ 1. **Limit the review.** Address the requested scope. A completed phase may motivate recommending cleanup, not starting it without a request. For a whole-state cleanup, inspect global, CWD, and session ownership explicitly; for a narrower request, inspect only affected owners. Use targeted reads for gaps, contradictions, ownership, or verification; do not rerun the project.
23
23
  2. **Classify.** Put user requirements and binding confirmed decisions in `contract`, observations, assistant conclusions, and unresolved work in `working`, chosen actions in `intents`, and inactive reusable detail in `lazy`. Never give an assistant conclusion user authority. Remove fulfilled, abandoned, superseded, or impossible intents; retain consequential results. Possibilities are not commitments.
24
24
  3. **Keep evidence boundaries.** Preserve corrections, prerequisites, bounded negative results, and useful uncertainty. Separate requirements, decisions, observations, conclusions, and hypotheses. Silence is not acceptance; repetition is not verification. One implementation's failure does not reject an approach. Neither freeze provisional methods nor reopen confirmed decisions without grounds.
25
25
  4. **Compact for continuation.** Remove duplicates, obsolete progress, unsupported claims, and secrets. Keep sufficient results, real retrieval pointers, pending interaction, and known next checks. Observations are not live external facts. Keep `lazy` shallow and priority-ordered. Recognize optional structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings as semantic-state references; other resources retain native locators. No reference form proves authority or existence, authorizes execution, or implies completion. Never scan or resolve references merely to find broken ones. When the bounded review independently needs a reference, a missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat the top-level hint as provenance and reconciliation guidance, never as requested state or proof of staleness; its paths are runtime-verified current owners, while no hint does not prove invention. Inspect ownership only as needed, then patch a proven stale owning value while preserving surrounding meaning. Effective absence or external inaccessibility is insufficient.
@@ -27,9 +27,9 @@ Follow the installed runtime contract. In active mode, satisfy all pending acqui
27
27
 
28
28
  ## Transfer only when needed
29
29
 
30
- Resolve destination conflicts without overwriting stronger or unrelated knowledge. Write the destination, retain the source, and verify the destination separately. Recheck source changes before deleting or narrowing it in a later patch. Reconcile affected references; verify source cleanup and effective inheritance. Never combine destination creation with source deletion.
30
+ Resolve destination conflicts without overwriting stronger or unrelated knowledge. For a proven move between scopes of one State Flow store, inspect both owners, then use one atomic multi-scope `patch_state` for destination and source changes. Verify both owners and effective inheritance afterward; reconcile affected references. A rejected cohort leaves neither side partially accepted.
31
31
 
32
- External transfers also require confirmed destination and write authority. Verify accepted content and a content-bound revision or receipt through the external interface, not memory. Preserve the source when acceptance is ambiguous. Never export secrets or broaden sensitive material without authorization.
32
+ External transfers require confirmed destination and write authority. Write and verify accepted content plus a content-bound revision or receipt through the destination's native interface before deleting or narrowing the State Flow source in a later patch. Recheck the source for intervening changes. Preserve it when acceptance is ambiguous. Never export secrets or broaden sensitive material without authorization.
33
33
 
34
34
  ## Apply, verify, stop
35
35
 
@@ -37,4 +37,4 @@ A fresh executor must recover constraints, results, open questions, commitments,
37
37
 
38
38
  Patch only material changes with `patch_state`, alone per assistant response; await acceptance. Never edit backing files, `response`, configuration, or runtime metadata. Read changed owner paths; inspect parent keys for deletions and effective state for inheritance changes.
39
39
 
40
- After rejection or interruption, inspect accepted state before bounded recovery. Report unresolved checks and partial transfers without dumping memory or implying historical erasure. Active iterations need accepted `final:true` before the answer; use a final-only call when no changes remain. Passive turns do not. Stop after this review, including when nothing needs changing.
40
+ After rejection or interruption, inspect accepted state before bounded recovery. Report unresolved checks and partial transfers without dumping memory or implying historical erasure. Before answering, apply only material durable changes; when nothing needs changing, make no `patch_state` call. Stop after this review, including when nothing needs changing.