@ornexus/neocortex 4.59.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 (197) hide show
  1. package/LICENSE +56 -0
  2. package/LICENSE-COMMERCIAL.md +70 -0
  3. package/README.md +58 -0
  4. package/dist/sbom.cdx.json +7067 -0
  5. package/docs/install/coderabbit-manual-setup.md +86 -0
  6. package/docs/install/installer-diagnostics.md +107 -0
  7. package/docs/install/linux-global-install.md +97 -0
  8. package/install.js +572 -0
  9. package/install.ps1 +2214 -0
  10. package/install.sh +2013 -0
  11. package/package.json +118 -0
  12. package/packages/client/dist/adapters/adapter-registry.d.ts +61 -0
  13. package/packages/client/dist/adapters/adapter-registry.js +1 -0
  14. package/packages/client/dist/adapters/antigravity-adapter.d.ts +18 -0
  15. package/packages/client/dist/adapters/antigravity-adapter.js +2 -0
  16. package/packages/client/dist/adapters/claude-code-adapter.d.ts +19 -0
  17. package/packages/client/dist/adapters/claude-code-adapter.js +3 -0
  18. package/packages/client/dist/adapters/codex-adapter.d.ts +19 -0
  19. package/packages/client/dist/adapters/codex-adapter.js +2 -0
  20. package/packages/client/dist/adapters/cursor-adapter.d.ts +19 -0
  21. package/packages/client/dist/adapters/cursor-adapter.js +4 -0
  22. package/packages/client/dist/adapters/gemini-adapter.d.ts +18 -0
  23. package/packages/client/dist/adapters/gemini-adapter.js +2 -0
  24. package/packages/client/dist/adapters/index.d.ts +19 -0
  25. package/packages/client/dist/adapters/index.js +1 -0
  26. package/packages/client/dist/adapters/platform-detector.d.ts +48 -0
  27. package/packages/client/dist/adapters/platform-detector.js +1 -0
  28. package/packages/client/dist/adapters/target-adapter.d.ts +70 -0
  29. package/packages/client/dist/adapters/target-adapter.js +0 -0
  30. package/packages/client/dist/adapters/vscode-adapter.d.ts +19 -0
  31. package/packages/client/dist/adapters/vscode-adapter.js +2 -0
  32. package/packages/client/dist/agent/refresh-stubs.d.ts +80 -0
  33. package/packages/client/dist/agent/refresh-stubs.js +2 -0
  34. package/packages/client/dist/agent/update-agent-yaml.d.ts +26 -0
  35. package/packages/client/dist/agent/update-agent-yaml.js +1 -0
  36. package/packages/client/dist/agent/update-description.d.ts +45 -0
  37. package/packages/client/dist/agent/update-description.js +1 -0
  38. package/packages/client/dist/cache/crypto-utils.d.ts +30 -0
  39. package/packages/client/dist/cache/crypto-utils.js +1 -0
  40. package/packages/client/dist/cache/encrypted-cache.d.ts +30 -0
  41. package/packages/client/dist/cache/encrypted-cache.js +1 -0
  42. package/packages/client/dist/cache/in-memory-asset-cache.d.ts +62 -0
  43. package/packages/client/dist/cache/in-memory-asset-cache.js +1 -0
  44. package/packages/client/dist/cache/index.d.ts +13 -0
  45. package/packages/client/dist/cache/index.js +1 -0
  46. package/packages/client/dist/cache/protected-pi-boundary.d.ts +19 -0
  47. package/packages/client/dist/cache/protected-pi-boundary.js +1 -0
  48. package/packages/client/dist/checkpoint/checkpoint-client-reader.d.ts +45 -0
  49. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -0
  50. package/packages/client/dist/checkpoint/index.d.ts +12 -0
  51. package/packages/client/dist/checkpoint/index.js +1 -0
  52. package/packages/client/dist/checkpoint/shared-checkpoint-types.d.ts +85 -0
  53. package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -0
  54. package/packages/client/dist/cli.d.ts +14 -0
  55. package/packages/client/dist/cli.js +48 -0
  56. package/packages/client/dist/commands/activate.d.ts +55 -0
  57. package/packages/client/dist/commands/activate.js +8 -0
  58. package/packages/client/dist/commands/cache-status.d.ts +39 -0
  59. package/packages/client/dist/commands/cache-status.js +2 -0
  60. package/packages/client/dist/commands/invoke.d.ts +229 -0
  61. package/packages/client/dist/commands/invoke.js +63 -0
  62. package/packages/client/dist/commands/refresh-memory.d.ts +11 -0
  63. package/packages/client/dist/commands/refresh-memory.js +1 -0
  64. package/packages/client/dist/config/resolver-selection.d.ts +40 -0
  65. package/packages/client/dist/config/resolver-selection.js +1 -0
  66. package/packages/client/dist/config/secure-config.d.ts +78 -0
  67. package/packages/client/dist/config/secure-config.js +12 -0
  68. package/packages/client/dist/constants.d.ts +25 -0
  69. package/packages/client/dist/constants.js +1 -0
  70. package/packages/client/dist/context/context-collector.d.ts +28 -0
  71. package/packages/client/dist/context/context-collector.js +2 -0
  72. package/packages/client/dist/context/context-sanitizer.d.ts +28 -0
  73. package/packages/client/dist/context/context-sanitizer.js +1 -0
  74. package/packages/client/dist/continuity/continuity-client-state-store.d.ts +183 -0
  75. package/packages/client/dist/continuity/continuity-client-state-store.js +1 -0
  76. package/packages/client/dist/continuity/invoke-hooks.d.ts +18 -0
  77. package/packages/client/dist/continuity/invoke-hooks.js +1 -0
  78. package/packages/client/dist/continuity/migrations/001-initial-schema.d.ts +11 -0
  79. package/packages/client/dist/continuity/migrations/001-initial-schema.js +263 -0
  80. package/packages/client/dist/continuity/sqlite-store.d.ts +409 -0
  81. package/packages/client/dist/continuity/sqlite-store.js +226 -0
  82. package/packages/client/dist/errors/error-messages.d.ts +40 -0
  83. package/packages/client/dist/errors/error-messages.js +2 -0
  84. package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +31 -0
  85. package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -0
  86. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.d.ts +77 -0
  87. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -0
  88. package/packages/client/dist/i18n/first-run.d.ts +23 -0
  89. package/packages/client/dist/i18n/first-run.js +2 -0
  90. package/packages/client/dist/index.d.ts +56 -0
  91. package/packages/client/dist/index.js +1 -0
  92. package/packages/client/dist/license/index.d.ts +5 -0
  93. package/packages/client/dist/license/index.js +1 -0
  94. package/packages/client/dist/license/license-client.d.ts +79 -0
  95. package/packages/client/dist/license/license-client.js +1 -0
  96. package/packages/client/dist/machine/fingerprint.d.ts +34 -0
  97. package/packages/client/dist/machine/fingerprint.js +2 -0
  98. package/packages/client/dist/machine/index.d.ts +5 -0
  99. package/packages/client/dist/machine/index.js +1 -0
  100. package/packages/client/dist/memory/project-memory-writer.d.ts +74 -0
  101. package/packages/client/dist/memory/project-memory-writer.js +36 -0
  102. package/packages/client/dist/memory/shared-project-memory-types.d.ts +370 -0
  103. package/packages/client/dist/memory/shared-project-memory-types.js +2 -0
  104. package/packages/client/dist/policy/architecture-policy.d.ts +40 -0
  105. package/packages/client/dist/policy/architecture-policy.js +2 -0
  106. package/packages/client/dist/policy/index.d.ts +8 -0
  107. package/packages/client/dist/policy/index.js +1 -0
  108. package/packages/client/dist/policy/shared-policy-types.d.ts +89 -0
  109. package/packages/client/dist/policy/shared-policy-types.js +0 -0
  110. package/packages/client/dist/resilience/circuit-breaker.d.ts +70 -0
  111. package/packages/client/dist/resilience/circuit-breaker.js +1 -0
  112. package/packages/client/dist/resilience/degradation-manager.d.ts +67 -0
  113. package/packages/client/dist/resilience/degradation-manager.js +1 -0
  114. package/packages/client/dist/resilience/freshness-indicator.d.ts +59 -0
  115. package/packages/client/dist/resilience/freshness-indicator.js +1 -0
  116. package/packages/client/dist/resilience/index.d.ts +8 -0
  117. package/packages/client/dist/resilience/index.js +1 -0
  118. package/packages/client/dist/resilience/recovery-detector.d.ts +59 -0
  119. package/packages/client/dist/resilience/recovery-detector.js +1 -0
  120. package/packages/client/dist/resolvers/asset-resolver.d.ts +79 -0
  121. package/packages/client/dist/resolvers/asset-resolver.js +0 -0
  122. package/packages/client/dist/resolvers/local-resolver.d.ts +26 -0
  123. package/packages/client/dist/resolvers/local-resolver.js +8 -0
  124. package/packages/client/dist/resolvers/remote-resolver.d.ts +91 -0
  125. package/packages/client/dist/resolvers/remote-resolver.js +1 -0
  126. package/packages/client/dist/runner/cli.d.ts +121 -0
  127. package/packages/client/dist/runner/cli.js +20 -0
  128. package/packages/client/dist/runner/scheduler.d.ts +116 -0
  129. package/packages/client/dist/runner/scheduler.js +6 -0
  130. package/packages/client/dist/runner-cli.d.ts +9 -0
  131. package/packages/client/dist/runner-cli.js +3 -0
  132. package/packages/client/dist/state/project-state-snapshot.d.ts +15 -0
  133. package/packages/client/dist/state/project-state-snapshot.js +1 -0
  134. package/packages/client/dist/state/state-json-repair.d.ts +17 -0
  135. package/packages/client/dist/state/state-json-repair.js +3 -0
  136. package/packages/client/dist/telemetry/index.d.ts +5 -0
  137. package/packages/client/dist/telemetry/index.js +1 -0
  138. package/packages/client/dist/telemetry/offline-queue.d.ts +57 -0
  139. package/packages/client/dist/telemetry/offline-queue.js +1 -0
  140. package/packages/client/dist/tier/index.d.ts +5 -0
  141. package/packages/client/dist/tier/index.js +1 -0
  142. package/packages/client/dist/tier/tier-aware-client.d.ts +105 -0
  143. package/packages/client/dist/tier/tier-aware-client.js +1 -0
  144. package/packages/client/dist/types/index.d.ts +140 -0
  145. package/packages/client/dist/types/index.js +1 -0
  146. package/packages/client/dist/yoloop/discovery-hook.d.ts +85 -0
  147. package/packages/client/dist/yoloop/discovery-hook.js +2 -0
  148. package/packages/client/dist/yoloop/index.d.ts +10 -0
  149. package/packages/client/dist/yoloop/index.js +1 -0
  150. package/packages/client/dist/yoloop/invoke-hooks.d.ts +125 -0
  151. package/packages/client/dist/yoloop/invoke-hooks.js +5 -0
  152. package/packages/client/dist/yoloop/shared-discover-epics.d.ts +289 -0
  153. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -0
  154. package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +172 -0
  155. package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -0
  156. package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +124 -0
  157. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -0
  158. package/postinstall.js +754 -0
  159. package/targets-stubs/antigravity/README.md +36 -0
  160. package/targets-stubs/antigravity/gemini.md +29 -0
  161. package/targets-stubs/antigravity/install-antigravity.sh +153 -0
  162. package/targets-stubs/antigravity/mcp-config.json +30 -0
  163. package/targets-stubs/antigravity/skill/SKILL.md +159 -0
  164. package/targets-stubs/claude-code/.mcp.json +32 -0
  165. package/targets-stubs/claude-code/README.md +20 -0
  166. package/targets-stubs/claude-code/neocortex-root.agent.yaml +42 -0
  167. package/targets-stubs/claude-code/neocortex-root.md +310 -0
  168. package/targets-stubs/claude-code/neocortex.agent.yaml +42 -0
  169. package/targets-stubs/claude-code/neocortex.md +378 -0
  170. package/targets-stubs/codex/AGENTS.md +244 -0
  171. package/targets-stubs/codex/README.md +47 -0
  172. package/targets-stubs/codex/config-mcp.toml +22 -0
  173. package/targets-stubs/codex/install-codex.sh +63 -0
  174. package/targets-stubs/codex/neocortex.toml +29 -0
  175. package/targets-stubs/cursor/README.md +33 -0
  176. package/targets-stubs/cursor/agent.md +204 -0
  177. package/targets-stubs/cursor/install-cursor.sh +50 -0
  178. package/targets-stubs/cursor/mcp.json +30 -0
  179. package/targets-stubs/gemini-cli/README.md +34 -0
  180. package/targets-stubs/gemini-cli/agent.md +234 -0
  181. package/targets-stubs/gemini-cli/agents/neocortex.md +54 -0
  182. package/targets-stubs/gemini-cli/gemini.md +46 -0
  183. package/targets-stubs/gemini-cli/install-gemini.sh +70 -0
  184. package/targets-stubs/gemini-cli/settings-mcp.json +30 -0
  185. package/targets-stubs/kimi/mcp.json +33 -0
  186. package/targets-stubs/kimi/neocortex.md +54 -0
  187. package/targets-stubs/lib/mcp-merge.js +189 -0
  188. package/targets-stubs/openclaw/README.md +12 -0
  189. package/targets-stubs/openclaw/SKILL.md +88 -0
  190. package/targets-stubs/opencode/neocortex-root.md +261 -0
  191. package/targets-stubs/opencode/neocortex.md +59 -0
  192. package/targets-stubs/opencode/opencode-mcp.json +35 -0
  193. package/targets-stubs/vscode/README.md +34 -0
  194. package/targets-stubs/vscode/copilot-instructions.md +47 -0
  195. package/targets-stubs/vscode/install-vscode.sh +72 -0
  196. package/targets-stubs/vscode/mcp.json +36 -0
  197. package/targets-stubs/vscode/neocortex.agent.md +245 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * P117.07: Inlined byte-identical types from `@neocortex/shared/types/yoloop-persistence`
3
+ * + `@neocortex/shared/yoloop/recovery-types` (RECOVERY_HISTORY_CAP).
4
+ *
5
+ * Same regression class as P117.01 (checkpoint inline). Mirrors P92.01 server
6
+ * SSoT-Shadow precedent — server `shared-types.ts` does the same for tier-config.
7
+ *
8
+ * Live regression: client tarball is published standalone via `@ornexus/neocortex`
9
+ * and does NOT include `@neocortex/shared` as a runtime dependency (workspace
10
+ * package, excluded from tarball per `.npmignore` + `tsconfig.build.json`). Any
11
+ * non-erased import of `@neocortex/shared` in `packages/client/dist/yoloop/**`
12
+ * causes runtime `ERR_MODULE_NOT_FOUND` on globally installed `@ornexus/neocortex`,
13
+ * silently disabling P101.06/P101.07 yoloop client state persistence (warning on
14
+ * every CLI invocation, fail-soft per design but feature effectively off).
15
+ *
16
+ * Sync responsibility: `yoloop-shared-sync.test.ts` (client-side drift test) +
17
+ * `shared-types-sync.test.ts` (server-side cross-package sanity, P92.03 baseline).
18
+ *
19
+ * Defense-in-depth: `validate-pre-publish.js` ships `validateClientNoSharedImport()`
20
+ * (P117.01 expanded by P117.07 to cover yoloop dist) — match = exit 1 (publish blocked).
21
+ *
22
+ * DO NOT EDIT WITHOUT also editing `packages/shared/src/types/yoloop-persistence.ts`
23
+ * + `packages/shared/src/yoloop/recovery-types.ts`.
24
+ */
25
+ /** Mode under which yoloop was invoked. Mirrors server-internal `YoloopSessionMode`. */
26
+ export type YoloopSessionMode = 'directory' | 'epic-file' | 'epic-id';
27
+ export type YoloopLockOwnerKind = 'client' | 'server';
28
+ export type YoloopLockOwnerPidState = 'live' | 'stale' | 'unknown';
29
+ export type YoloopLockLeaseState = 'active' | 'expired' | 'unknown';
30
+ /** Lock file payload. Mirrors server-internal `LockMetadata` (v1.1 schema). */
31
+ export interface YoloopPersistedLock {
32
+ readonly pid: number;
33
+ readonly hostname: string;
34
+ /**
35
+ * Additive ownership marker. Missing means legacy/server-owned semantics:
36
+ * remote host locks are honored conservatively.
37
+ */
38
+ readonly ownerKind?: YoloopLockOwnerKind;
39
+ /**
40
+ * Request-scoped liveness classification produced by the client before
41
+ * echoing the snapshot. It is optional and may be omitted from persisted
42
+ * lock files.
43
+ */
44
+ readonly ownerPidState?: YoloopLockOwnerPidState;
45
+ /**
46
+ * Public-safe lease/session marker. Opaque; clients must not render local
47
+ * paths, hostnames, raw PID command lines, or other private machine details
48
+ * from it. Missing fields mean legacy v1.1 lock semantics.
49
+ */
50
+ readonly leaseId?: string;
51
+ readonly leaseState?: YoloopLockLeaseState;
52
+ readonly leaseAcquiredAt?: string;
53
+ readonly leaseDurationMs?: number;
54
+ readonly leaseExpiresAt?: string;
55
+ readonly startedAt: string;
56
+ readonly sessionId: string;
57
+ readonly mode: YoloopSessionMode;
58
+ readonly inputPath: string;
59
+ readonly epicQueue: readonly string[];
60
+ readonly currentEpicIndex: number;
61
+ readonly currentEpicId: string;
62
+ readonly ttlExpiresAt: string;
63
+ /** Backward-compat v1.0 mirror of `currentEpicId`. */
64
+ readonly epicId?: string;
65
+ }
66
+ /** Per-session block inside the state file. Mirrors server-internal `YoloopSession`. */
67
+ export interface YoloopPersistedSession {
68
+ readonly mode: YoloopSessionMode;
69
+ readonly inputPath: string;
70
+ readonly epicQueue: readonly string[];
71
+ readonly currentEpicIndex: number;
72
+ readonly iterationsThisRun: number;
73
+ readonly startedAt: string;
74
+ readonly warnings: readonly string[];
75
+ /** P96.05 AC1: storyId -> strike count within THIS session. */
76
+ readonly failedStories?: Readonly<Record<string, number>>;
77
+ /** P96.05 AC8: storyId -> ISO timestamp of last failure (5min cool-down gate). */
78
+ readonly lastFailureAt?: Readonly<Record<string, string>>;
79
+ /** P96.05 AC2: storyIds marked `status='blocked'` in state.json this session. */
80
+ readonly blockedStories?: readonly string[];
81
+ /** P96.05 AC5: storyIds skipped due to `*yolo` timeout. */
82
+ readonly skippedStories?: readonly string[];
83
+ }
84
+ /**
85
+ * Forward-declared opaque reference to `YoloopRecoveryAttempt` (canonical lives
86
+ * in `@neocortex/shared/yoloop/recovery-types`). Structural mirror to avoid
87
+ * circular import. Drift enforced by `shared-types-sync.test.ts` (P104.06 block).
88
+ */
89
+ export interface YoloopRecoveryAttemptRef {
90
+ readonly attempted_at: string;
91
+ readonly story_id: string;
92
+ readonly failure_category: string;
93
+ readonly recovery_trigger: string;
94
+ readonly recovery_step_id_tokenized: string;
95
+ readonly outcome: 'pending' | 'succeeded' | 'failed';
96
+ readonly finished_at: string | null;
97
+ }
98
+ /**
99
+ * Persistent counters block (everything except the lock).
100
+ * Mirrors server-internal `YoloopRuntimeState` minus lock-related fields.
101
+ */
102
+ export interface YoloopPersistedCounters {
103
+ readonly version: '1.0' | '1.1';
104
+ readonly lastResetAt: string;
105
+ readonly storiesRunToday: number;
106
+ readonly iterationsToday: number;
107
+ readonly iterationsTodayDate: string;
108
+ readonly tokensUsedToday: number;
109
+ readonly lastRunAt: string | null;
110
+ readonly coolDownUntil: string | null;
111
+ readonly totalRunsAllTime: number;
112
+ readonly totalStoriesAllTime: number;
113
+ readonly currentSession: YoloopPersistedSession | null;
114
+ /**
115
+ * P104.06: FIFO bounded audit trail of self-healing recovery attempts.
116
+ * Optional for backward compat (clients pre-P104.06 omit the field; readers
117
+ * default to []). Cap enforced via `RECOVERY_HISTORY_CAP` below.
118
+ */
119
+ readonly recovery_history?: readonly YoloopRecoveryAttemptRef[];
120
+ }
121
+ /**
122
+ * Full yoloop state snapshot that the client echoes to the server on each
123
+ * invoke. `lock` is null when no run is in flight.
124
+ */
125
+ export interface YoloopPersistedState {
126
+ readonly lock: YoloopPersistedLock | null;
127
+ readonly state: YoloopPersistedCounters;
128
+ }
129
+ /**
130
+ * Typed mutation operations the server returns for the client to apply.
131
+ * Clients apply operations in order, atomically. If any operation fails the
132
+ * client SHOULD NOT apply subsequent ones.
133
+ */
134
+ export type YoloopStateOperation = {
135
+ readonly type: 'acquire_lock';
136
+ readonly lock: YoloopPersistedLock;
137
+ } | {
138
+ readonly type: 'release_lock';
139
+ } | {
140
+ readonly type: 'write_state';
141
+ readonly state: YoloopPersistedCounters;
142
+ } | {
143
+ readonly type: 'advance_epic';
144
+ readonly nextEpicIndex: number;
145
+ readonly nextEpicId: string;
146
+ } | {
147
+ readonly type: 'no_op';
148
+ } | {
149
+ readonly type: 'recovery_attempted';
150
+ readonly attempt: YoloopRecoveryAttemptRef;
151
+ } | {
152
+ readonly type: 'recovery_resolved';
153
+ readonly attempt_id: string;
154
+ readonly outcome: 'succeeded' | 'failed';
155
+ readonly evidence?: string;
156
+ };
157
+ /**
158
+ * Server -> client update contract. Client applies `operations` in order to
159
+ * its local yoloop state store.
160
+ */
161
+ export interface YoloopStateUpdate {
162
+ readonly operations: ReadonlyArray<YoloopStateOperation>;
163
+ readonly lock?: YoloopPersistedLock | null;
164
+ readonly state?: YoloopPersistedCounters;
165
+ }
166
+ /**
167
+ * P104.06 -- FIFO cap for `state.recovery_history[]` in
168
+ * `.neocortex/yoloop-state.json`. When the array exceeds this size, the
169
+ * client drops oldest entries. Bound is heuristic: ~50 attempts × ~300
170
+ * bytes/entry = ~15 KB worst case in state.json (negligible).
171
+ */
172
+ export declare const RECOVERY_HISTORY_CAP = 50;
@@ -0,0 +1 @@
1
+ const R=50;export{R as RECOVERY_HISTORY_CAP};
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Epic P101.06 -- YoloopClientStateStore.
3
+ *
4
+ * Client-side counterpart of the (soon-to-be-deleted) server YoloopStateStore.
5
+ * Owns `.neocortex/yoloop.lock` and `.neocortex/yoloop-state.json` in the
6
+ * user's local project workspace.
7
+ *
8
+ * The client is the ONLY filesystem writer for yoloop state after P101
9
+ * lands. The server is pure: it reads state echoed by the client via
10
+ * `stateSnapshot.yoloop` and returns `metadata.yoloopStateUpdate` with an
11
+ * operation log the client applies here.
12
+ *
13
+ * Atomicity guarantees (P96.03):
14
+ * - acquireLock uses fs.open(... 'wx') -- POSIX exclusive create
15
+ * - writeState uses tmp + rename (POSIX atomic rename)
16
+ *
17
+ * Fail-open philosophy (P61.04 / P66.01):
18
+ * - chmod / ACL failure -> warn log, NEVER blocks operation
19
+ * - release on missing lock is idempotent (no-op)
20
+ * - readAll returns defaults on any I/O or parse error
21
+ *
22
+ * Cross-platform: Unix chmod + Windows icacls via existing
23
+ * setSecureFilePermissions helper from secure-config.ts.
24
+ */
25
+ import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopStateUpdate } from './shared-yoloop-types.js';
26
+ /** Minimal Pino-compatible logger subset. */
27
+ export interface ClientYoloopLogger {
28
+ warn(payload: object, msg?: string): void;
29
+ info(payload: object, msg?: string): void;
30
+ error(payload: object, msg?: string): void;
31
+ }
32
+ /** Build default counters for a fresh project (no state file present). */
33
+ export declare function createDefaultCounters(nowIso: string): YoloopPersistedCounters;
34
+ /** Build default state envelope (no lock, default counters). */
35
+ export declare function createDefaultState(nowIso: string): YoloopPersistedState;
36
+ export declare class YoloopClientStateStore {
37
+ private readonly stateDir;
38
+ private readonly statePath;
39
+ private readonly lockPath;
40
+ private readonly logger;
41
+ constructor(projectRoot: string, logger?: ClientYoloopLogger);
42
+ /**
43
+ * Read lock + state in a single call. Used by invoke.ts pre-request hook
44
+ * to populate `stateSnapshot.yoloop` for the server. Handles ENOENT
45
+ * gracefully by returning default state.
46
+ */
47
+ readAll(nowIso?: string): Promise<YoloopPersistedState>;
48
+ /**
49
+ * Apply server-provided operations in order. Called by invoke.ts
50
+ * post-response hook. Operations are applied atomically per-op; if one
51
+ * fails, subsequent ops are NOT applied and the caller (invoke.ts)
52
+ * decides whether to log-and-continue or abort.
53
+ *
54
+ * Contract: partial application is safe because each operation is a
55
+ * single atomic filesystem mutation. Next invoke's readAll() reconciles.
56
+ */
57
+ applyUpdate(update: YoloopStateUpdate, nowIso?: string): Promise<void>;
58
+ private applyOperation;
59
+ /**
60
+ * Append a new recovery attempt to `state.recovery_history[]`. FIFO cap
61
+ * via `capRecoveryHistory`. Idempotent against retry storms: duplicate
62
+ * (story_id + attempted_at) entries are skipped with a warn log.
63
+ */
64
+ private appendRecoveryAttempt;
65
+ /**
66
+ * Resolve a pending recovery attempt by tokenized step id. Updates
67
+ * `outcome` and stamps `finished_at`. If the attempt was already evicted
68
+ * by the FIFO cap or never existed, emits a warn log (`recovery_resolve_orphan`)
69
+ * but does not throw -- bound state cap is a deliberate trade-off.
70
+ */
71
+ private resolveRecoveryAttempt;
72
+ /**
73
+ * Atomically acquire the yoloop lock. On EEXIST, performs PID liveness
74
+ * check (same-host) and retries once if orphan detected. Cross-host
75
+ * locks are honored (cannot signal across hosts).
76
+ *
77
+ * The server's `acquire_lock` operation provides the lock payload; we
78
+ * overwrite the `pid` and `hostname` fields with OUR values (the client
79
+ * is the authoritative writer of pid/hostname; server can't know them).
80
+ */
81
+ private acquireLock;
82
+ /** Release the lock. Idempotent (ENOENT swallowed). */
83
+ private releaseLock;
84
+ /** Read lock file. Returns null on ENOENT / parse error. */
85
+ private readLock;
86
+ /** Read state file; returns defaults on any I/O or parse error. */
87
+ private readState;
88
+ /** Atomic state write: temp + rename. */
89
+ private writeState;
90
+ /**
91
+ * Advance to next epic: read-modify-write on currentSession.currentEpicIndex.
92
+ * Fail-open if no active session (logs warn; subsequent *yoloop will
93
+ * emit a fresh acquire_lock).
94
+ */
95
+ private advanceEpic;
96
+ private ensureDir;
97
+ /**
98
+ * Fire-and-forget chmod/ACL. Uses sync helpers from secure-config.ts
99
+ * which are fail-open by contract (never throw).
100
+ */
101
+ private trySetFilePerms;
102
+ private trySetDirPerms;
103
+ /**
104
+ * Normalize a lock before echoing it to the server. The local client can
105
+ * check PID liveness for client-owned same-host locks; the server cannot.
106
+ */
107
+ private prepareLockForSnapshot;
108
+ /**
109
+ * If the existing lock is held by a dead PID on the same host, delete
110
+ * it and return true (caller retries acquisition). Otherwise return
111
+ * false (honor the lock).
112
+ */
113
+ private maybeRecoverOrphanLock;
114
+ private deleteLockBestEffort;
115
+ private isSameClientSessionLock;
116
+ /** Remove `yoloop-state.json.tmp.*` orphans from crashed writes. */
117
+ private cleanupOrphanTempFiles;
118
+ }
119
+ /**
120
+ * Enforce FIFO bound on `recovery_history[]`. Returns a new array of length
121
+ * <= `RECOVERY_HISTORY_CAP`, preserving the most recent entries (drops from
122
+ * head). Pure: input is not mutated.
123
+ */
124
+ export declare function capRecoveryHistory(history: readonly YoloopRecoveryAttemptRef[]): readonly YoloopRecoveryAttemptRef[];
@@ -0,0 +1 @@
1
+ import{promises as a}from"node:fs";import{join as p,dirname as g}from"node:path";import{hostname as h}from"node:os";import{setSecureFilePermissions as k,setSecureDirPermissions as m}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as y}from"./shared-yoloop-types.js";const S=".neocortex",_="yoloop-state.json",A="yoloop.lock",E=360*60*1e3,D={warn:()=>{},info:()=>{},error:()=>{}};function u(i){const t=i.slice(0,10);return{version:"1.1",lastResetAt:i,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:t,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null}}function q(i){return{lock:null,state:u(i)}}class b{stateDir;statePath;lockPath;logger;constructor(t,e=D){this.stateDir=p(t,S),this.statePath=p(this.stateDir,_),this.lockPath=p(this.stateDir,A),this.logger=e}async readAll(t){const e=t??new Date().toISOString();await this.cleanupOrphanTempFiles();const[r,o]=await Promise.all([this.readLock(),this.readState(e)]);return{lock:await this.prepareLockForSnapshot(r,e),state:o}}async applyUpdate(t,e){const r=e??new Date().toISOString();await this.ensureDir();for(const o of t.operations)await this.applyOperation(o,r)}async applyOperation(t,e){switch(t.type){case"acquire_lock":await this.acquireLock(t.lock,e);return;case"release_lock":await this.releaseLock();return;case"write_state":await this.writeState(t.state);return;case"advance_epic":await this.advanceEpic(t.nextEpicIndex,t.nextEpicId,e);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"recovery_attempted":await this.appendRecoveryAttempt(t.attempt,e);return;case"recovery_resolved":await this.resolveRecoveryAttempt(t.attempt_id,t.outcome,e);return;default:{const r=t;this.logger.warn({event:"yoloop_unknown_op",op:r},"unknown yoloop operation; skipping");return}}}async appendRecoveryAttempt(t,e){const r=await this.readState(e),o=r.recovery_history??[];if(o.some(l=>l.attempted_at===t.attempted_at&&l.story_id===t.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:t.story_id},"duplicate recovery_attempted op; skipping");return}const n=w([...o,t]),c={...r,recovery_history:n};await this.writeState(c),this.logger.info({event:"recovery_attempted_persisted",story_id:t.story_id,history_size:n.length},"recovery attempt persisted")}async resolveRecoveryAttempt(t,e,r){const o=await this.readState(r),s=o.recovery_history??[],n=s.findIndex(v=>v.recovery_step_id_tokenized===t);if(n===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:t},"recovery_resolved op had no matching attempt; skipping");return}const c=s[n];if(c.outcome===e&&c.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:t,outcome:e},"duplicate recovery_resolved op; skipping");return}const l=s.slice();l[n]={...c,outcome:e,finished_at:r};const d={...o,recovery_history:l};await this.writeState(d),this.logger.info({event:"recovery_resolved_persisted",attempt_id:t,outcome:e},"recovery attempt resolved")}async acquireLock(t,e){const r={...t,pid:process.pid,hostname:h(),ownerKind:"client",startedAt:t.startedAt||e,leaseId:t.leaseId??P(t.sessionId,e),leaseState:"active",leaseAcquiredAt:t.leaseAcquiredAt??e,leaseDurationMs:t.leaseDurationMs??E,leaseExpiresAt:t.leaseExpiresAt??t.ttlExpiresAt},o=JSON.stringify(r,null,2);try{const s=await a.open(this.lockPath,"wx");try{await s.writeFile(o,"utf8")}finally{await s.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:r.pid,sessionId:r.sessionId},"yoloop lock acquired");return}catch(s){const n=s;if(n.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:n.message},"failed to acquire yoloop lock"),s;const c=await this.readLock();if(c&&this.isSameClientSessionLock(c,r)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:r.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(e))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),s;const d=await a.open(this.lockPath,"wx");try{await d.writeFile(o,"utf8")}finally{await d.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:r.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(){try{await a.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(t){const e=t;if(e.code==="ENOENT"){this.logger.info({event:"yoloop_lock_release_noop"},"yoloop lock release noop (file absent)");return}this.logger.warn({event:"yoloop_lock_release_error",err:e.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const t=await a.readFile(this.lockPath,"utf8"),e=JSON.parse(t),r=e.currentEpicId??e.epicId??"";return{pid:e.pid??0,hostname:e.hostname??"",...e.ownerKind?{ownerKind:e.ownerKind}:{},...e.ownerPidState?{ownerPidState:e.ownerPidState}:{},...typeof e.leaseId=="string"?{leaseId:e.leaseId}:{},...e.leaseState?{leaseState:e.leaseState}:{},...typeof e.leaseAcquiredAt=="string"?{leaseAcquiredAt:e.leaseAcquiredAt}:{},...typeof e.leaseDurationMs=="number"?{leaseDurationMs:e.leaseDurationMs}:{},...typeof e.leaseExpiresAt=="string"?{leaseExpiresAt:e.leaseExpiresAt}:{},startedAt:e.startedAt??"",sessionId:e.sessionId??"",mode:e.mode??"epic-id",inputPath:e.inputPath??"",epicQueue:e.epicQueue??(r?[r]:[]),currentEpicIndex:e.currentEpicIndex??0,currentEpicId:r,ttlExpiresAt:e.ttlExpiresAt??"",epicId:e.epicId??r}}catch{return null}}async readState(t){try{const e=await a.readFile(this.statePath,"utf8"),r=JSON.parse(e);return L(r,t)}catch(e){const r=e;return r.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:r.message},"yoloop state file unreadable; using defaults"),u(t)}}async writeState(t){const e=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,r=JSON.stringify(t,null,2);try{await a.writeFile(e,r,"utf8"),this.trySetFilePerms(e),await a.rename(e,this.statePath)}catch(o){const s=o;this.logger.error({event:"yoloop_state_write_error",err:s.message},"failed to write yoloop state");try{await a.unlink(e)}catch{}throw o}}async advanceEpic(t,e,r){const o=await this.readState(r);if(!o.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:e},"advance_epic with no active session; skipping");return}const s={...o,currentSession:{...o.currentSession,currentEpicIndex:t}};await this.writeState(s),this.logger.info({event:"yoloop_epic_advance",nextEpicId:e,nextIndex:t},"yoloop advanced to next epic")}async ensureDir(){try{await a.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(t){const e=t;throw this.logger.error({event:"yoloop_mkdir_error",err:e.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),t}}trySetFilePerms(t){try{k(t)}catch{}}trySetDirPerms(t){try{m(t)}catch{}}async prepareLockForSnapshot(t,e){if(!t)return null;if(t.ownerKind!=="client")return t;const r=f(t,e),o={...t,leaseState:r};if(r==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"lease_expired"},"expired client-owned yoloop lock lease recovered before snapshot echo"),null;if(!o.pid||o.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"invalid_pid"},"invalid PID in client-owned yoloop lock; recovered"),null;if(o.hostname&&o.hostname!==h())return{...o,ownerPidState:"unknown"};try{return process.kill(o.pid,0),{...o,ownerPidState:"live"}}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"dead_pid"},"stale client-owned yoloop lock recovered before snapshot echo"),null}}async maybeRecoverOrphanLock(t){const e=await this.readLock();if(!e)return!0;if(f(e,t)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!e.pid||e.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"invalid_pid"},"invalid PID in lock file; recovered"),!0;if(e.hostname&&e.hostname!==h())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:e.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(e.pid,0),!1}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"dead_pid"},"orphan yoloop lock recovered"),!0}}async deleteLockBestEffort(){try{await a.unlink(this.lockPath)}catch{}}isSameClientSessionLock(t,e){return t.ownerKind==="client"&&t.sessionId===e.sessionId&&t.pid===process.pid&&t.hostname===h()}async cleanupOrphanTempFiles(){try{const t=g(this.statePath),e=await a.readdir(t),r=`${_}.tmp.`,o=e.filter(s=>s.startsWith(r));for(const s of o)try{await a.unlink(p(t,s))}catch{}}catch{}}}function P(i,t){return`lease-${x(`${i}:${t}`)}`}function x(i){let t=2166136261;for(let e=0;e<i.length;e+=1)t^=i.charCodeAt(e),t=Math.imul(t,16777619)>>>0;return t.toString(36).padStart(7,"0")}function f(i,t){const e=i.leaseExpiresAt??i.ttlExpiresAt,r=Date.parse(e),o=Date.parse(t);return!Number.isFinite(r)||!Number.isFinite(o)?"unknown":r<=o?"expired":"active"}function L(i,t){const e=u(t);return{version:"1.1",lastResetAt:i.lastResetAt??e.lastResetAt,storiesRunToday:i.storiesRunToday??0,iterationsToday:i.iterationsToday??0,iterationsTodayDate:i.iterationsTodayDate??e.iterationsTodayDate,tokensUsedToday:i.tokensUsedToday??0,lastRunAt:i.lastRunAt??null,coolDownUntil:i.coolDownUntil??null,totalRunsAllTime:i.totalRunsAllTime??0,totalStoriesAllTime:i.totalStoriesAllTime??0,currentSession:i.currentSession??null,recovery_history:Array.isArray(i.recovery_history)?w(i.recovery_history):[]}}function w(i){return i.length<=y?i:i.slice(i.length-y)}export{b as YoloopClientStateStore,w as capRecoveryHistory,u as createDefaultCounters,q as createDefaultState};