@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.
- package/LICENSE +56 -0
- package/LICENSE-COMMERCIAL.md +70 -0
- package/README.md +58 -0
- package/dist/sbom.cdx.json +7067 -0
- package/docs/install/coderabbit-manual-setup.md +86 -0
- package/docs/install/installer-diagnostics.md +107 -0
- package/docs/install/linux-global-install.md +97 -0
- package/install.js +572 -0
- package/install.ps1 +2214 -0
- package/install.sh +2013 -0
- package/package.json +118 -0
- package/packages/client/dist/adapters/adapter-registry.d.ts +61 -0
- package/packages/client/dist/adapters/adapter-registry.js +1 -0
- package/packages/client/dist/adapters/antigravity-adapter.d.ts +18 -0
- package/packages/client/dist/adapters/antigravity-adapter.js +2 -0
- package/packages/client/dist/adapters/claude-code-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/claude-code-adapter.js +3 -0
- package/packages/client/dist/adapters/codex-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/codex-adapter.js +2 -0
- package/packages/client/dist/adapters/cursor-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/cursor-adapter.js +4 -0
- package/packages/client/dist/adapters/gemini-adapter.d.ts +18 -0
- package/packages/client/dist/adapters/gemini-adapter.js +2 -0
- package/packages/client/dist/adapters/index.d.ts +19 -0
- package/packages/client/dist/adapters/index.js +1 -0
- package/packages/client/dist/adapters/platform-detector.d.ts +48 -0
- package/packages/client/dist/adapters/platform-detector.js +1 -0
- package/packages/client/dist/adapters/target-adapter.d.ts +70 -0
- package/packages/client/dist/adapters/target-adapter.js +0 -0
- package/packages/client/dist/adapters/vscode-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/vscode-adapter.js +2 -0
- package/packages/client/dist/agent/refresh-stubs.d.ts +80 -0
- package/packages/client/dist/agent/refresh-stubs.js +2 -0
- package/packages/client/dist/agent/update-agent-yaml.d.ts +26 -0
- package/packages/client/dist/agent/update-agent-yaml.js +1 -0
- package/packages/client/dist/agent/update-description.d.ts +45 -0
- package/packages/client/dist/agent/update-description.js +1 -0
- package/packages/client/dist/cache/crypto-utils.d.ts +30 -0
- package/packages/client/dist/cache/crypto-utils.js +1 -0
- package/packages/client/dist/cache/encrypted-cache.d.ts +30 -0
- package/packages/client/dist/cache/encrypted-cache.js +1 -0
- package/packages/client/dist/cache/in-memory-asset-cache.d.ts +62 -0
- package/packages/client/dist/cache/in-memory-asset-cache.js +1 -0
- package/packages/client/dist/cache/index.d.ts +13 -0
- package/packages/client/dist/cache/index.js +1 -0
- package/packages/client/dist/cache/protected-pi-boundary.d.ts +19 -0
- package/packages/client/dist/cache/protected-pi-boundary.js +1 -0
- package/packages/client/dist/checkpoint/checkpoint-client-reader.d.ts +45 -0
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -0
- package/packages/client/dist/checkpoint/index.d.ts +12 -0
- package/packages/client/dist/checkpoint/index.js +1 -0
- package/packages/client/dist/checkpoint/shared-checkpoint-types.d.ts +85 -0
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -0
- package/packages/client/dist/cli.d.ts +14 -0
- package/packages/client/dist/cli.js +48 -0
- package/packages/client/dist/commands/activate.d.ts +55 -0
- package/packages/client/dist/commands/activate.js +8 -0
- package/packages/client/dist/commands/cache-status.d.ts +39 -0
- package/packages/client/dist/commands/cache-status.js +2 -0
- package/packages/client/dist/commands/invoke.d.ts +229 -0
- package/packages/client/dist/commands/invoke.js +63 -0
- package/packages/client/dist/commands/refresh-memory.d.ts +11 -0
- package/packages/client/dist/commands/refresh-memory.js +1 -0
- package/packages/client/dist/config/resolver-selection.d.ts +40 -0
- package/packages/client/dist/config/resolver-selection.js +1 -0
- package/packages/client/dist/config/secure-config.d.ts +78 -0
- package/packages/client/dist/config/secure-config.js +12 -0
- package/packages/client/dist/constants.d.ts +25 -0
- package/packages/client/dist/constants.js +1 -0
- package/packages/client/dist/context/context-collector.d.ts +28 -0
- package/packages/client/dist/context/context-collector.js +2 -0
- package/packages/client/dist/context/context-sanitizer.d.ts +28 -0
- package/packages/client/dist/context/context-sanitizer.js +1 -0
- package/packages/client/dist/continuity/continuity-client-state-store.d.ts +183 -0
- package/packages/client/dist/continuity/continuity-client-state-store.js +1 -0
- package/packages/client/dist/continuity/invoke-hooks.d.ts +18 -0
- package/packages/client/dist/continuity/invoke-hooks.js +1 -0
- package/packages/client/dist/continuity/migrations/001-initial-schema.d.ts +11 -0
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +263 -0
- package/packages/client/dist/continuity/sqlite-store.d.ts +409 -0
- package/packages/client/dist/continuity/sqlite-store.js +226 -0
- package/packages/client/dist/errors/error-messages.d.ts +40 -0
- package/packages/client/dist/errors/error-messages.js +2 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +31 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -0
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.d.ts +77 -0
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -0
- package/packages/client/dist/i18n/first-run.d.ts +23 -0
- package/packages/client/dist/i18n/first-run.js +2 -0
- package/packages/client/dist/index.d.ts +56 -0
- package/packages/client/dist/index.js +1 -0
- package/packages/client/dist/license/index.d.ts +5 -0
- package/packages/client/dist/license/index.js +1 -0
- package/packages/client/dist/license/license-client.d.ts +79 -0
- package/packages/client/dist/license/license-client.js +1 -0
- package/packages/client/dist/machine/fingerprint.d.ts +34 -0
- package/packages/client/dist/machine/fingerprint.js +2 -0
- package/packages/client/dist/machine/index.d.ts +5 -0
- package/packages/client/dist/machine/index.js +1 -0
- package/packages/client/dist/memory/project-memory-writer.d.ts +74 -0
- package/packages/client/dist/memory/project-memory-writer.js +36 -0
- package/packages/client/dist/memory/shared-project-memory-types.d.ts +370 -0
- package/packages/client/dist/memory/shared-project-memory-types.js +2 -0
- package/packages/client/dist/policy/architecture-policy.d.ts +40 -0
- package/packages/client/dist/policy/architecture-policy.js +2 -0
- package/packages/client/dist/policy/index.d.ts +8 -0
- package/packages/client/dist/policy/index.js +1 -0
- package/packages/client/dist/policy/shared-policy-types.d.ts +89 -0
- package/packages/client/dist/policy/shared-policy-types.js +0 -0
- package/packages/client/dist/resilience/circuit-breaker.d.ts +70 -0
- package/packages/client/dist/resilience/circuit-breaker.js +1 -0
- package/packages/client/dist/resilience/degradation-manager.d.ts +67 -0
- package/packages/client/dist/resilience/degradation-manager.js +1 -0
- package/packages/client/dist/resilience/freshness-indicator.d.ts +59 -0
- package/packages/client/dist/resilience/freshness-indicator.js +1 -0
- package/packages/client/dist/resilience/index.d.ts +8 -0
- package/packages/client/dist/resilience/index.js +1 -0
- package/packages/client/dist/resilience/recovery-detector.d.ts +59 -0
- package/packages/client/dist/resilience/recovery-detector.js +1 -0
- package/packages/client/dist/resolvers/asset-resolver.d.ts +79 -0
- package/packages/client/dist/resolvers/asset-resolver.js +0 -0
- package/packages/client/dist/resolvers/local-resolver.d.ts +26 -0
- package/packages/client/dist/resolvers/local-resolver.js +8 -0
- package/packages/client/dist/resolvers/remote-resolver.d.ts +91 -0
- package/packages/client/dist/resolvers/remote-resolver.js +1 -0
- package/packages/client/dist/runner/cli.d.ts +121 -0
- package/packages/client/dist/runner/cli.js +20 -0
- package/packages/client/dist/runner/scheduler.d.ts +116 -0
- package/packages/client/dist/runner/scheduler.js +6 -0
- package/packages/client/dist/runner-cli.d.ts +9 -0
- package/packages/client/dist/runner-cli.js +3 -0
- package/packages/client/dist/state/project-state-snapshot.d.ts +15 -0
- package/packages/client/dist/state/project-state-snapshot.js +1 -0
- package/packages/client/dist/state/state-json-repair.d.ts +17 -0
- package/packages/client/dist/state/state-json-repair.js +3 -0
- package/packages/client/dist/telemetry/index.d.ts +5 -0
- package/packages/client/dist/telemetry/index.js +1 -0
- package/packages/client/dist/telemetry/offline-queue.d.ts +57 -0
- package/packages/client/dist/telemetry/offline-queue.js +1 -0
- package/packages/client/dist/tier/index.d.ts +5 -0
- package/packages/client/dist/tier/index.js +1 -0
- package/packages/client/dist/tier/tier-aware-client.d.ts +105 -0
- package/packages/client/dist/tier/tier-aware-client.js +1 -0
- package/packages/client/dist/types/index.d.ts +140 -0
- package/packages/client/dist/types/index.js +1 -0
- package/packages/client/dist/yoloop/discovery-hook.d.ts +85 -0
- package/packages/client/dist/yoloop/discovery-hook.js +2 -0
- package/packages/client/dist/yoloop/index.d.ts +10 -0
- package/packages/client/dist/yoloop/index.js +1 -0
- package/packages/client/dist/yoloop/invoke-hooks.d.ts +125 -0
- package/packages/client/dist/yoloop/invoke-hooks.js +5 -0
- package/packages/client/dist/yoloop/shared-discover-epics.d.ts +289 -0
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -0
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +172 -0
- package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +124 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -0
- package/postinstall.js +754 -0
- package/targets-stubs/antigravity/README.md +36 -0
- package/targets-stubs/antigravity/gemini.md +29 -0
- package/targets-stubs/antigravity/install-antigravity.sh +153 -0
- package/targets-stubs/antigravity/mcp-config.json +30 -0
- package/targets-stubs/antigravity/skill/SKILL.md +159 -0
- package/targets-stubs/claude-code/.mcp.json +32 -0
- package/targets-stubs/claude-code/README.md +20 -0
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +42 -0
- package/targets-stubs/claude-code/neocortex-root.md +310 -0
- package/targets-stubs/claude-code/neocortex.agent.yaml +42 -0
- package/targets-stubs/claude-code/neocortex.md +378 -0
- package/targets-stubs/codex/AGENTS.md +244 -0
- package/targets-stubs/codex/README.md +47 -0
- package/targets-stubs/codex/config-mcp.toml +22 -0
- package/targets-stubs/codex/install-codex.sh +63 -0
- package/targets-stubs/codex/neocortex.toml +29 -0
- package/targets-stubs/cursor/README.md +33 -0
- package/targets-stubs/cursor/agent.md +204 -0
- package/targets-stubs/cursor/install-cursor.sh +50 -0
- package/targets-stubs/cursor/mcp.json +30 -0
- package/targets-stubs/gemini-cli/README.md +34 -0
- package/targets-stubs/gemini-cli/agent.md +234 -0
- package/targets-stubs/gemini-cli/agents/neocortex.md +54 -0
- package/targets-stubs/gemini-cli/gemini.md +46 -0
- package/targets-stubs/gemini-cli/install-gemini.sh +70 -0
- package/targets-stubs/gemini-cli/settings-mcp.json +30 -0
- package/targets-stubs/kimi/mcp.json +33 -0
- package/targets-stubs/kimi/neocortex.md +54 -0
- package/targets-stubs/lib/mcp-merge.js +189 -0
- package/targets-stubs/openclaw/README.md +12 -0
- package/targets-stubs/openclaw/SKILL.md +88 -0
- package/targets-stubs/opencode/neocortex-root.md +261 -0
- package/targets-stubs/opencode/neocortex.md +59 -0
- package/targets-stubs/opencode/opencode-mcp.json +35 -0
- package/targets-stubs/vscode/README.md +34 -0
- package/targets-stubs/vscode/copilot-instructions.md +47 -0
- package/targets-stubs/vscode/install-vscode.sh +72 -0
- package/targets-stubs/vscode/mcp.json +36 -0
- 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};
|