@ornexus/neocortex 4.60.43 → 4.60.45
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/dist/sbom.cdx.json +23 -23
- package/install.js +16 -1
- package/install.ps1 +57 -401
- package/install.sh +30 -37
- package/package.json +2 -2
- package/packages/client/dist/commands/invoke.js +39 -39
- package/packages/client/dist/errors/error-messages.js +2 -2
- package/packages/client/dist/evidence/collector.d.ts +2 -0
- package/packages/client/dist/evidence/collector.js +1 -1
- package/packages/client/dist/license/license-client.d.ts +11 -0
- package/packages/client/dist/license/license-client.js +1 -1
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +9 -0
- package/packages/client/dist/runner/headless/run-yoloop.js +5 -3
- package/packages/client/dist/yoloop/index.d.ts +1 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +10 -8
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +61 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +23 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
- package/packages/shared/dist/invocation/index.js +9 -2
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/install-antigravity.sh +10 -101
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/codex/install-codex.sh +4 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/cursor/install-cursor.sh +4 -1
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/gemini-cli/install-gemini.sh +4 -1
- package/targets-stubs/grok/install-grok.sh +4 -1
- package/targets-stubs/lib/mcp-merge.js +220 -20
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/install-vscode.sh +4 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import{statSync as
|
|
2
|
-
`);return}}async function
|
|
3
|
-
`)}}function n(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`),{applied
|
|
7
|
-
`),{applied:!1,blocking:!0,reasonCode:
|
|
1
|
+
import{statSync as Z}from"node:fs";import{join as K}from"node:path";import{YoloopClientStateStore as D}from"./yoloop-client-state-store.js";import{isYoloopInvocation as W}from"./target-normalization.js";import{applyStateMigration as q,StateMigrationApplyError as J}from"../migrations/state-json.js";function X(e){return W(e)}const Q=/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i;function le(e){if(typeof e.args!="string"||e.args.length===0)return;const i=e.args.match(Q);if(!i)return;const o=i[0];if(e.args.slice(o.length).trim()!=="")return;let s=!1;try{const t=K(e.projectRoot,"docs","epics");s=Z(t).isDirectory()}catch(t){const d=t instanceof Error?t.message:String(t);e.logger?.warn?.(`[Neocortex] Zero-config *yoloop hook (P121.05): could not stat docs/epics/: ${d}. Proceeding with raw args.`);return}s&&(e.args=`${o} @docs/epics/`,e.logger?.info?.("[Neocortex] Zero-config *yoloop (P121.05): auto-resolved empty args to @docs/epics/"))}async function de(e,i,o){if(X(e))try{return await new D(i).readAll(o)}catch(r){process.stderr.write(`[yoloop] Failed to read local state: ${r.message}. Continuing without state.
|
|
2
|
+
`);return}}async function pe(e,i,o){if(!e)return;const r=e.yoloopStateUpdate;if(!(!r||!Array.isArray(r.operations)||r.operations.length===0))try{await new D(i).applyUpdate(r,o)}catch(s){const t=s.message??String(s);process.stderr.write(`[yoloop] Failed to persist state: ${t}. Your next *yoloop may repeat or skip an operation. Run *yoloop again to retry.
|
|
3
|
+
`)}}function n(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function T(e,i){return Array.isArray(e)&&e.length<=i&&e.every(o=>typeof o=="string"&&o.length>0&&o.length<=160&&/^[A-Za-z0-9_.:+-]+$/.test(o))}function L(e){return n(e.scope)?e.scope.kind==="projected"||typeof e.scope.omittedStoryCount=="number"&&e.scope.omittedStoryCount>0||typeof e.scope.omittedEpicCount=="number"&&e.scope.omittedEpicCount>0:!1}function ee(e){const i={...e};return delete i.scope,delete i.stateSummary,delete i.graphRetrieval,delete i.runnerBootstrap,delete i.projectEvidence,delete i.continuity,delete i.implementCheckpoint,i}function te(e){return typeof e=="string"&&["done","completed","merged"].includes(e.trim().toLowerCase())}function oe(e){return/^[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$/.test(e)&&!e.includes("..")&&!e.includes("//")&&!e.includes("@{")&&!e.endsWith("/")&&!e.endsWith(".")&&!e.endsWith(".lock")}async function ie(e,i){const o=e.syntheticStory;if(!n(o))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-mutation-missing"};const r=o.schemaVersion,s=o.epicId,t=o.storyId,d=o.storyPath,m=o.deterministicKey,b=o.storyContent,_=o.storyContentSha256,p=o.lifecycle;if(r!==2||typeof s!="string"||!/^P9\d{18}$/.test(s)||typeof t!="string"||t!==`${s}.01.000001`||typeof d!="string"||d!==`docs/stories/${t}.story.md`||typeof m!="string"||!/^[a-f0-9]{16}$/.test(m)||typeof _!="string"||!/^[a-f0-9]{64}$/.test(_)||typeof b!="string"||b.length===0||b.length>131072||!n(p)||p.branchMode!=="epic"||p.executionBranch!==`epic/${s}`||typeof p.originBranch!="string"||!oe(p.originBranch)||typeof p.graphRevision!="string"||!/^[a-f0-9]{64}$/.test(p.graphRevision)||p.isolatedSyntheticEpic!==!0||o.publicSafe!==!0||o.sanitized!==!0)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-mutation-invalid"};const l=await import("node:fs/promises"),h=await import("node:path"),A=await import("node:crypto");if(A.createHash("sha256").update(b).digest("hex")!==_)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-content-integrity-invalid"};const C=h.join(i,".neocortex"),I=h.join(C,"state.json"),w=h.join(i,...d.split("/"));let P;try{P=await l.readFile(I,"utf8")}catch{return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-missing"}}const R=A.createHash("sha256").update(P).digest("hex");let u;try{u=JSON.parse(P)}catch{return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-corrupt"}}if(!n(u)||!n(u.stories)||!n(u.epics)||L(u))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-not-full"};if(!n(u.config)||u.config.default_branch!==p.originBranch)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-origin-branch-conflict"};const c=u.stories[t];if(c!==void 0&&(!n(c)||c.epic_id!==s||c.story_file!==d||!n(c.inlineYolo)||c.inlineYolo.deterministicKey!==m))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-conflict"};const y=u.epics[s];if(y!==void 0&&(!n(y)||y.id!==s||y.source!=="inline-yolo-materialization"||y.branch_mode!=="epic"||y.execution_branch!==p.executionBranch||y.origin_branch!==p.originBranch||y.graph_revision!==p.graphRevision||!n(y.inlineYoloLifecycle)||y.inlineYoloLifecycle.deterministicKey!==m||y.inlineYoloLifecycle.isolatedSyntheticEpic!==!0||!Array.isArray(y.stories)))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-conflict"};const $=n(y)?y:{},S=Array.isArray($.stories)?$.stories:[];if(S.some(a=>typeof a!="string")||new Set(S).size!==S.length)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-story-list-invalid"};for(const a of S){const f=u.stories[a];if(!n(f)||f.id!==a||f.epic_id!==s)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-ownership-conflict"}}try{const a=await l.readFile(w,"utf8"),f=a.includes(`id: ${t}`)&&a.includes(`synthetic_key: ${m}`);if(a!==b&&!f)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-file-conflict"}}catch(a){if(a.code!=="ENOENT")return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-file-unreadable"}}const k=S.includes(t)?[...S]:[...S,t],v={...u.stories,[t]:{...n(c)?c:{},id:t,epic_id:s,title:"Synthetic inline USER STORY for yolo",status:n(c)&&typeof c.status=="string"?c.status:"backlog",steps_completed:n(c)&&Array.isArray(c.steps_completed)?c.steps_completed:[],last_step:n(c)?c.last_step??null:null,branch_name:n(c)?c.branch_name??null:null,story_file:d,source:"inline-yolo-materialization",inlineYolo:{...n(c)&&n(c.inlineYolo)?c.inlineYolo:{},schemaVersion:2,deterministicKey:m,publicSafe:!0,sanitized:!0,materializedBeforeExecution:!0,ephemeralExecutionForbidden:!0}}},x=k.filter(a=>n(v[a])&&te(v[a].status)).length,j=n(u.inlineYolo)?u.inlineYolo:{},G=n(j.materializations)?j.materializations:{},U=ee({...u,stories:v,epics:{...u.epics,[s]:{...$,id:s,title:typeof $.title=="string"?$.title:"Isolated synthetic inline yolo lifecycle",status:x===k.length&&k.length>0?"done":"in-progress",source:"inline-yolo-materialization",branch_mode:"epic",execution_branch:p.executionBranch,origin_branch:p.originBranch,graph_revision:p.graphRevision,stories:k,total_stories:k.length,completed_stories:x,...x===k.length&&k.length>0?{}:{completed_at:null},inlineYoloLifecycle:{schemaVersion:2,deterministicKey:m,isolatedSyntheticEpic:!0}}},inlineYolo:{...j,schemaVersion:2,materializations:{...G,[t]:{schemaVersion:2,epicId:s,storyId:t,storyPath:d,deterministicKey:m,storyContentSha256:_,sourceStateSha256:R,lifecycle:p,publicSafe:!0,sanitized:!0,materializedBeforeExecution:!0,ephemeralExecutionForbidden:!0,pipelineTrigger:`*yolo @${d}`,reasonCodes:Array.isArray(o.reasonCodes)?o.reasonCodes:[]}}},last_updated:new Date().toISOString()}),M=`${Date.now()}-${A.randomBytes(6).toString("hex")}`,Y=h.join(C,`state.json.tmp.${M}`),O=`${w}.tmp.${M}`,F=h.join(C,`state.json.backup-${new Date().toISOString().replace(/[:.]/g,"-")}`),H=h.join(C,"state.json.inline-yolo.lock");let N=!1,E;const V=async a=>{let f;try{f=await l.open(a,"r"),await f.sync()}catch(g){const z=g.code;if(!["EINVAL","ENOTSUP","EPERM","EISDIR"].includes(z??""))throw g}finally{await f?.close().catch(()=>{})}},B=async(a,f)=>{const g=await l.open(a,"wx",384);try{await g.writeFile(f,"utf8"),await g.sync()}finally{await g.close()}};try{await l.mkdir(C,{recursive:!0}),await l.mkdir(h.dirname(w),{recursive:!0});try{E=await l.open(H,"wx",384),await E.writeFile(`${process.pid}
|
|
4
|
+
${R}
|
|
5
|
+
`,"utf8"),await E.sync()}catch(z){if(z.code==="EEXIST")return{applied:!1,blocking:!0,reasonCode:"inline-yolo-writeback-locked"};throw z}const a=await l.readFile(I,"utf8");if(A.createHash("sha256").update(a).digest("hex")!==R)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-cas-conflict"};try{await l.access(w)}catch{await B(O,b),await l.rename(O,w),await V(h.dirname(w)),N=!0}await l.copyFile(I,F);const g=await l.open(F,"r");try{await g.sync()}finally{await g.close()}return await B(Y,JSON.stringify(U,null,2)),await l.rename(Y,I),await V(C),process.stderr.write(`[Neocortex] inline yolo story materialized with bounded full-state merge; story=${t}; backup=${h.basename(F)}.
|
|
6
|
+
`),{applied:!0,blocking:!1,reasonCode:"inline-yolo-materialized"}}catch(a){await Promise.allSettled([l.unlink(Y),l.unlink(O)]),N&&await l.readFile(w,"utf8").catch(()=>{})===b&&await l.unlink(w).catch(()=>{});const f=a.message??String(a);return process.stderr.write(`[Neocortex] inline yolo materialization failed: ${f}.
|
|
7
|
+
`),{applied:!1,blocking:!0,reasonCode:"inline-yolo-persistence-failed"}}finally{await E?.close().catch(()=>{}),E&&await l.unlink(H).catch(()=>{})}}async function ue(e,i){if(!e||e.stateMutated!==!0)return{applied:!1,blocking:!1,reasonCode:"state-writeback-not-requested"};if(e.stateMutationStrategy==="merge-inline-yolo-v2"||e.stateMutationReason==="inline-yolo-materialized-synthetic-story")return ie(e,i);if(e.stateMutationStrategy==="state-migration-v1"||e.stateMigrationPlan!==void 0){const r=e.stateMigrationPlan,s=new Set(["schemaVersion","operationId","sourceSchemaVersion","targetSchemaVersion","sourceCanonicalHash","targetCanonicalHash","steps","migratedEpicIds","migratedStoryIds","preservedUnknownFieldCount","reasonCodes"]);if(!n(r)||Object.keys(r).some(t=>!s.has(t))||r.schemaVersion!=="StateMigrationPlan/1.0"||typeof r.operationId!="string"||!/^smig_[a-f0-9]{32}$/.test(r.operationId)||typeof r.sourceCanonicalHash!="string"||!/^[a-f0-9]{64}$/.test(r.sourceCanonicalHash)||typeof r.targetCanonicalHash!="string"||!/^[a-f0-9]{64}$/.test(r.targetCanonicalHash)||![0,1,"absent"].includes(r.sourceSchemaVersion)||r.targetSchemaVersion!==1||!Array.isArray(r.steps)||r.steps.length>8192||r.steps.some(t=>!n(t)||Object.keys(t).some(d=>!["kind","source","target"].includes(d))||!["initialize-collection","merge-epic","merge-story","preserve-legacy","remove-legacy","set-schema-version"].includes(String(t.kind))||typeof t.target!="string"||t.target.length>512||!/^[A-Za-z0-9_.:[\]-]+$/.test(t.target)||t.source!==void 0&&(typeof t.source!="string"||t.source.length>512||!/^[A-Za-z0-9_.:[\]-]+$/.test(t.source)))||!T(r.migratedEpicIds,4096)||!T(r.migratedStoryIds,4096)||!T(r.reasonCodes,32)||!Number.isSafeInteger(r.preservedUnknownFieldCount)||r.preservedUnknownFieldCount<0)return{applied:!1,blocking:!0,reasonCode:"state-migration-plan-missing-or-invalid"};try{const t=q(i,r),d=`.neocortex/state.json.migration-backup-${t.operationId}`;return process.stderr.write(`[P220-EV-STATE-MIGRATION] outcome=${t.outcome}; operation=${t.operationId}; backup=${t.backupHash?d:"none"}.
|
|
8
|
+
`),{applied:t.outcome==="applied",blocking:!1,reasonCode:t.outcome==="applied"?"state-migration-applied":"state-migration-replay-verified"}}catch(t){const d=t instanceof J?t.reasonCode:"state-migration-apply-failed";return process.stderr.write(`[P220-EV-STATE-MIGRATION] blocked=${d}.
|
|
9
|
+
`),{applied:!1,blocking:!0,reasonCode:d}}}const o=e.migratedState;return!o||typeof o!="object"||Array.isArray(o)?{applied:!1,blocking:!0,reasonCode:"state-writeback-payload-missing"}:L(o)?(process.stderr.write(`[Neocortex] Refused projected/partial state replacement; canonical state was not modified.
|
|
8
10
|
`),{applied:!1,blocking:!0,reasonCode:"state-writeback-partial-rejected"}):(process.stderr.write(`[P220-EV-STATE-MIGRATION] blocked=state-migration-plan-missing-or-invalid; canonical state was not modified.
|
|
9
|
-
`),{applied:!1,blocking:!0,reasonCode:"state-migration-plan-missing-or-invalid"})}async function
|
|
11
|
+
`),{applied:!1,blocking:!0,reasonCode:"state-migration-plan-missing-or-invalid"})}async function ye(e){if(!e.featureFlag)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"flag off"};try{return await(await import("./discovery-hook.js")).runDiscoveryHook(e)}catch(i){const o=i instanceof Error?i.message:String(i);return e.logger?.warn?.(`[neocortex] discovery hook (lazy load) failed: ${o}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`lazy load error: ${o}`}}}async function fe(e){if(!e.featureFlag)return{applied:!1,reason:"flag_off"};try{return await(await import("../graph-retrieval/pre-command-hook.js")).runGraphRetrievalHook(e)}catch(i){const o=i instanceof Error?i.message:String(i);return e.logger?.warn?.(`[neocortex] graph retrieval hook (lazy load) failed: ${o}. Proceeding without enrichment.`),{applied:!1,reason:"hook_error",warning:o}}}export{X as isYoloopTrigger,le as maybeAutoResolveYoloopArgs,ye as maybeRunDiscoveryHook,fe as maybeRunGraphRetrievalHook,ue as persistMigratedStateFromResponse,pe as persistYoloopFromResponse,de as readYoloopForRequest};
|
|
@@ -27,6 +27,7 @@ export type YoloopSessionMode = 'directory' | 'epic-file' | 'epic-id';
|
|
|
27
27
|
export type YoloopLockOwnerKind = 'client' | 'server';
|
|
28
28
|
export type YoloopLockOwnerPidState = 'live' | 'stale' | 'unknown';
|
|
29
29
|
export type YoloopLockLeaseState = 'active' | 'expired' | 'unknown';
|
|
30
|
+
export type YoloopPersistenceCapability = 'rollover-session-v1';
|
|
30
31
|
/** Lock file payload. Mirrors server-internal `LockMetadata` (v1.1 schema). */
|
|
31
32
|
export interface YoloopPersistedLock {
|
|
32
33
|
readonly pid: number;
|
|
@@ -139,6 +140,14 @@ export interface YoloopRunTransitionReceiptV1 {
|
|
|
139
140
|
readonly snapshotHash: string | null;
|
|
140
141
|
readonly status: YoloopRunStatus;
|
|
141
142
|
}
|
|
143
|
+
export interface YoloopRunPredecessorV1 {
|
|
144
|
+
readonly schemaVersion: 'YoloopRunPredecessor/1.0';
|
|
145
|
+
readonly runId: string;
|
|
146
|
+
readonly revision: number;
|
|
147
|
+
readonly snapshotHash: string | null;
|
|
148
|
+
readonly successorOperationId: string;
|
|
149
|
+
readonly publicSafe: true;
|
|
150
|
+
}
|
|
142
151
|
export interface YoloopRunControlV1 {
|
|
143
152
|
readonly schemaVersion: 'YoloopRunControl/1.0';
|
|
144
153
|
readonly runId: string;
|
|
@@ -152,6 +161,31 @@ export interface YoloopRunControlV1 {
|
|
|
152
161
|
readonly progressSnapshots: readonly YoloopPersistedProgressSnapshotV1[];
|
|
153
162
|
readonly latestSnapshotHash: string | null;
|
|
154
163
|
readonly receipts: readonly YoloopRunTransitionReceiptV1[];
|
|
164
|
+
readonly predecessor?: YoloopRunPredecessorV1;
|
|
165
|
+
}
|
|
166
|
+
/** Public receipt for one compare-bound terminal-session rollover. */
|
|
167
|
+
export interface YoloopSessionRolloverReceiptV1 {
|
|
168
|
+
readonly schemaVersion: 'YoloopSessionRolloverReceipt/1.0';
|
|
169
|
+
readonly runId: string;
|
|
170
|
+
readonly operationId: string;
|
|
171
|
+
readonly idempotencyKey: string;
|
|
172
|
+
readonly payloadHash: string;
|
|
173
|
+
readonly coolDownUntil: string;
|
|
174
|
+
readonly revision: number;
|
|
175
|
+
readonly snapshotHash: string | null;
|
|
176
|
+
readonly status: 'exhausted';
|
|
177
|
+
readonly publicSafe: true;
|
|
178
|
+
}
|
|
179
|
+
/** Public root-driver evidence: terminal for this run, resumable for the queue. */
|
|
180
|
+
export interface YoloopResumableBoundaryV1 {
|
|
181
|
+
readonly schemaVersion: 'YoloopResumableBoundary/1.0';
|
|
182
|
+
readonly resumable: true;
|
|
183
|
+
readonly reason: 'YOLOOP_PER_RUN_CAP_REACHED' | 'YOLOOP_SESSION_TIMEOUT';
|
|
184
|
+
readonly coolDownUntil: string;
|
|
185
|
+
readonly retryAfterMs: number;
|
|
186
|
+
readonly operationId: string;
|
|
187
|
+
readonly receipt: YoloopSessionRolloverReceiptV1;
|
|
188
|
+
readonly publicSafe: true;
|
|
155
189
|
}
|
|
156
190
|
/**
|
|
157
191
|
* Forward-declared opaque reference to `YoloopRecoveryAttempt` (canonical lives
|
|
@@ -206,6 +240,7 @@ export interface YoloopPersistedCounters {
|
|
|
206
240
|
export interface YoloopPersistedState {
|
|
207
241
|
readonly lock: YoloopPersistedLock | null;
|
|
208
242
|
readonly state: YoloopPersistedCounters;
|
|
243
|
+
readonly capabilities?: readonly YoloopPersistenceCapability[];
|
|
209
244
|
readonly continuationRecoveryEvidence?: Readonly<Record<string, unknown>>;
|
|
210
245
|
}
|
|
211
246
|
/**
|
|
@@ -255,6 +290,32 @@ export type YoloopStateOperation = {
|
|
|
255
290
|
readonly expectedSnapshotHash: string | null;
|
|
256
291
|
readonly payloadHash: string;
|
|
257
292
|
readonly next: YoloopRunControlV1;
|
|
293
|
+
} | {
|
|
294
|
+
readonly type: 'start_successor_run';
|
|
295
|
+
readonly runId: string;
|
|
296
|
+
readonly previousRunId: string;
|
|
297
|
+
readonly operationId: string;
|
|
298
|
+
readonly idempotencyKey: string;
|
|
299
|
+
readonly expectedRevision: number;
|
|
300
|
+
readonly expectedSnapshotHash: string | null;
|
|
301
|
+
readonly payloadHash: string;
|
|
302
|
+
readonly next: YoloopRunControlV1;
|
|
303
|
+
} | {
|
|
304
|
+
readonly type: 'rollover_session';
|
|
305
|
+
readonly runId: string;
|
|
306
|
+
readonly operationId: string;
|
|
307
|
+
readonly idempotencyKey: string;
|
|
308
|
+
readonly expectedRevision: number;
|
|
309
|
+
readonly expectedSnapshotHash: string | null;
|
|
310
|
+
readonly expectedSessionId: string | null;
|
|
311
|
+
readonly expectedSessionFingerprint: string;
|
|
312
|
+
readonly expectedLeaseId: string | null;
|
|
313
|
+
readonly coolDownUntil: string;
|
|
314
|
+
readonly reason: 'YOLOOP_PER_RUN_CAP_REACHED' | 'YOLOOP_SESSION_TIMEOUT';
|
|
315
|
+
readonly exhaustedRun: YoloopRunControlV1 | null;
|
|
316
|
+
readonly payloadHash: string;
|
|
317
|
+
readonly next: YoloopPersistedCounters;
|
|
318
|
+
readonly receipt: YoloopSessionRolloverReceiptV1;
|
|
258
319
|
} | {
|
|
259
320
|
readonly type: 'recovery_attempted';
|
|
260
321
|
readonly attempt: YoloopRecoveryAttemptRef;
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* setSecureFilePermissions helper from secure-config.ts.
|
|
24
24
|
*/
|
|
25
25
|
import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopRunControlV1, YoloopRunTransitionReceiptV1, YoloopStateOperation, YoloopStateUpdate } from './shared-yoloop-types.js';
|
|
26
|
+
export declare const YOLOOP_PERSISTENCE_CAPABILITIES: readonly ["rollover-session-v1"];
|
|
27
|
+
type YoloopSessionRolloverOperation = Extract<YoloopStateOperation, {
|
|
28
|
+
readonly type: 'rollover_session';
|
|
29
|
+
}>;
|
|
26
30
|
/** Minimal Pino-compatible logger subset. */
|
|
27
31
|
export interface ClientYoloopLogger {
|
|
28
32
|
warn(payload: object, msg?: string): void;
|
|
@@ -38,6 +42,7 @@ export declare class YoloopClientStateStore {
|
|
|
38
42
|
private readonly statePath;
|
|
39
43
|
private readonly lockPath;
|
|
40
44
|
private readonly mutationLockPath;
|
|
45
|
+
private readonly rolloverManifestPath;
|
|
41
46
|
private readonly logger;
|
|
42
47
|
constructor(projectRoot: string, logger?: ClientYoloopLogger);
|
|
43
48
|
/**
|
|
@@ -104,6 +109,16 @@ export declare class YoloopClientStateStore {
|
|
|
104
109
|
private readState;
|
|
105
110
|
/** Durable atomic state write: same-filesystem temp + fsync + rename + dir fsync. */
|
|
106
111
|
private writeState;
|
|
112
|
+
/**
|
|
113
|
+
* Apply the per-run-cap boundary as one journaled logical transaction over
|
|
114
|
+
* yoloop-state.json and yoloop.lock. The mutation lock serializes local
|
|
115
|
+
* writers; the durable manifest makes every crash point resumable.
|
|
116
|
+
*/
|
|
117
|
+
private rolloverSession;
|
|
118
|
+
private completePreparedSessionRollover;
|
|
119
|
+
private recoverSessionRolloverIfPresent;
|
|
120
|
+
private readRolloverManifest;
|
|
121
|
+
private writeRolloverManifest;
|
|
107
122
|
/**
|
|
108
123
|
* Advance to next epic: read-modify-write on currentSession.currentEpicIndex.
|
|
109
124
|
* Fail-open if no active session (logs warn; subsequent *yoloop will
|
|
@@ -114,6 +129,8 @@ export declare class YoloopClientStateStore {
|
|
|
114
129
|
private syncStateDirectory;
|
|
115
130
|
private withMutationLock;
|
|
116
131
|
private transitionRun;
|
|
132
|
+
/** Replace one proven exhausted cap run with a fresh created successor. */
|
|
133
|
+
private startSuccessorRun;
|
|
117
134
|
/**
|
|
118
135
|
* Fire-and-forget chmod/ACL. Uses sync helpers from secure-config.ts
|
|
119
136
|
* which are fail-open by contract (never throw).
|
|
@@ -136,6 +153,11 @@ export declare class YoloopClientStateStore {
|
|
|
136
153
|
/** Remove `yoloop-state.json.tmp.*` orphans from crashed writes. */
|
|
137
154
|
private cleanupOrphanTempFiles;
|
|
138
155
|
}
|
|
156
|
+
export declare function fingerprintYoloopSession(session: NonNullable<YoloopPersistedCounters['currentSession']>): string;
|
|
157
|
+
export declare function buildYoloopSessionRolloverPayloadHash(operation: Omit<YoloopSessionRolloverOperation, 'type' | 'payloadHash' | 'receipt'>): string;
|
|
158
|
+
export declare function buildYoloopSuccessorRunPayloadHash(operation: Omit<Extract<YoloopStateOperation, {
|
|
159
|
+
readonly type: 'start_successor_run';
|
|
160
|
+
}>, 'type' | 'payloadHash'>): string;
|
|
139
161
|
export declare function buildYoloopRunTransitionPayloadHash(input: {
|
|
140
162
|
readonly runId: string;
|
|
141
163
|
readonly operationId: string;
|
|
@@ -150,3 +172,4 @@ export declare function buildYoloopRunTransitionPayloadHash(input: {
|
|
|
150
172
|
* head). Pure: input is not mutated.
|
|
151
173
|
*/
|
|
152
174
|
export declare function capRecoveryHistory(history: readonly YoloopRecoveryAttemptRef[]): readonly YoloopRecoveryAttemptRef[];
|
|
175
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{promises as u}from"node:fs";import{createHash as P}from"node:crypto";import{join as w,dirname as C}from"node:path";import{hostname as f}from"node:os";import{setSecureFilePermissions as k,setSecureDirPermissions as x}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as R}from"./shared-yoloop-types.js";const D=".neocortex",m="yoloop-state.json",H="yoloop.lock",Y="yoloop-state.mutation.lock",U=360*60*1e3,K=3e4,b=200,F={warn:()=>{},info:()=>{},error:()=>{}};function I(o){const e=o.slice(0,10);return{version:"1.1",lastResetAt:o,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:e,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null,continuationPolicy:"p205-finite-resume-v2",pendingContinuationAuthorities:[],continuationReceipts:[],continuationRecoveryReceipts:[]}}function oe(o){return{lock:null,state:I(o)}}class ne{stateDir;statePath;lockPath;mutationLockPath;logger;constructor(e,t=F){this.stateDir=w(e,D),this.statePath=w(this.stateDir,m),this.lockPath=w(this.stateDir,H),this.mutationLockPath=w(this.stateDir,Y),this.logger=t}async readAll(e){const t=e??new Date().toISOString();await this.cleanupOrphanTempFiles();const[n,r]=await Promise.all([this.readLock(),this.readState(t)]);return{lock:await this.prepareLockForSnapshot(n,t),state:r}}async applyUpdate(e,t){const n=t??new Date().toISOString();await this.ensureDir();for(const r of e.operations)await this.applyOperation(r,n)}async requestRunTransition(e,t){const n=t??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(()=>this.transitionRun(e,n))}async requestLifecycleAction(e,t={},n){const r=n??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(async()=>{const i=(await this.readState(r)).runControl;if(!i)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const a=Date.parse(r).toString(36),c=t.operationId??`op-${e}-${a}`,l=t.idempotencyKey??`idem-${e}-${a}`,d=i.receipts.find(_=>_.operationId===c||_.idempotencyKey===l);if(d){if(d.operationId===c&&d.idempotencyKey===l)return d;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}const p=M(i.status,e);if(!p)throw new Error(g.has(i.status)?"YOLOOP_RUN_TERMINAL":"YOLOOP_RUN_TRANSITION_INVALID");return this.transitionRun(V({current:i,action:e,target:p,operationId:c,idempotencyKey:l,occurredAt:r}),r)})}async applyOperation(e,t){switch(e.type){case"acquire_lock":await this.acquireLock(e.lock,t);return;case"release_lock":await this.releaseLock(e);return;case"write_state":await this.withMutationLock(async()=>{const n=await this.readState(t),{runControl:r,...s}=e.state;await this.writeState({...s,...n.runControl?{version:"1.2",runControl:n.runControl}:{}})});return;case"advance_epic":await this.advanceEpic(e.nextEpicIndex,e.nextEpicId,t);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"reserve_continuation_authority":await this.reserveContinuationAuthority(e.authority,t);return;case"acknowledge_continuation_authority":await this.acknowledgeContinuationAuthority(e.idempotencyKey,e.receipt,t);return;case"repair_continuation_authority":await this.repairContinuationAuthority(e.beforeHash,e.authority,e.receipt,t);return;case"switch_continuation_policy":await this.switchContinuationPolicy(e.policy,e.session,t);return;case"transition_run":await this.requestRunTransition(e,t);return;case"recovery_attempted":await this.appendRecoveryAttempt(e.attempt,t);return;case"recovery_resolved":await this.resolveRecoveryAttempt(e.attempt_id,e.outcome,t);return;default:{const n=e;this.logger.warn({event:"yoloop_unknown_op",op:n},"unknown yoloop operation; skipping");return}}}async reserveContinuationAuthority(e,t){const n=await this.readState(t),r=n.pendingContinuationAuthorities??[],s=typeof e.idempotencyKey=="string"?e.idempotencyKey:"";if(!s)throw new Error("YOLOOP_CONTINUATION_AUTHORITY_INVALID");const i=r.find(a=>a.idempotencyKey===s);if(i){if(h(i)===h(e))return;throw new Error("YOLOOP_CONTINUATION_AUTHORITY_CONFLICT")}if(r.length>=64)throw new Error("YOLOOP_PENDING_AUTHORITY_CAPACITY_EXCEEDED");await this.writeState({...n,pendingContinuationAuthorities:[...r,e]})}async acknowledgeContinuationAuthority(e,t,n){const r=await this.readState(n),s=r.pendingContinuationAuthorities??[],i=s.findIndex(d=>d.idempotencyKey===e);if(i<0)throw new Error("YOLOOP_CONTINUATION_RECEIPT_ORPHAN");const a=s[i];if(t.idempotencyKey!==e||t.sessionId!==a.sessionId||JSON.stringify(t.storyIds)!==JSON.stringify(a.storyIds))throw new Error("YOLOOP_CONTINUATION_RECEIPT_MISMATCH");const c=s.filter((d,p)=>p!==i),l=r.continuationReceipts??[];await this.writeState({...r,pendingContinuationAuthorities:c,continuationReceipts:[...l.slice(-63),t]})}async repairContinuationAuthority(e,t,n,r){const s=await this.readState(r),i=s.pendingContinuationAuthorities??[],a=s.continuationRecoveryReceipts??[],c=typeof n.idempotencyKey=="string"?n.idempotencyKey:"",l=a.find(_=>_.idempotencyKey===c);if(l){if(h(l)===h(n))return;throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_CONFLICT")}if(i.length!==1||y("p208-legacy-authority-before-v1",i[0])!==e)throw new Error("YOLOOP_CONTINUATION_RECOVERY_BEFORE_MISMATCH");const d=y("p208-legacy-authority-after-v1",t);if(n.beforeHash!==e||n.afterHash!==d)throw new Error("YOLOOP_CONTINUATION_RECOVERY_HASH_MISMATCH");const p={...n};if(delete p.idempotencyKey,!c||y("p208-legacy-authority-recovery-receipt-v1",p)!==c)throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_INVALID");await this.writeState({...s,pendingContinuationAuthorities:[t],continuationRecoveryReceipts:[...a.slice(-63),n]})}async switchContinuationPolicy(e,t,n){const r=await this.readState(n);if(r.currentSession||(r.pendingContinuationAuthorities?.length??0)>0)throw new Error("YOLOOP_POLICY_SWITCH_ACTIVE");try{await u.copyFile(this.statePath,`${this.statePath}.policy-backup`)}catch(s){if(s.code!=="ENOENT")throw s}await this.writeState({...r,continuationPolicy:e,continuationSession:t})}async appendRecoveryAttempt(e,t){const n=await this.readState(t),r=n.recovery_history??[];if(r.some(c=>c.attempted_at===e.attempted_at&&c.story_id===e.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:e.story_id},"duplicate recovery_attempted op; skipping");return}const i=T([...r,e]),a={...n,recovery_history:i};await this.writeState(a),this.logger.info({event:"recovery_attempted_persisted",story_id:e.story_id,history_size:i.length},"recovery attempt persisted")}async resolveRecoveryAttempt(e,t,n){const r=await this.readState(n),s=r.recovery_history??[],i=s.findIndex(d=>d.recovery_step_id_tokenized===e);if(i===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:e},"recovery_resolved op had no matching attempt; skipping");return}const a=s[i];if(a.outcome===t&&a.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:e,outcome:t},"duplicate recovery_resolved op; skipping");return}const c=s.slice();c[i]={...a,outcome:t,finished_at:n};const l={...r,recovery_history:c};await this.writeState(l),this.logger.info({event:"recovery_resolved_persisted",attempt_id:e,outcome:t},"recovery attempt resolved")}async acquireLock(e,t){const n={...e,pid:process.pid,hostname:f(),ownerKind:"client",startedAt:e.startedAt||t,leaseId:e.leaseId??j(e.sessionId,t),leaseState:"active",leaseAcquiredAt:e.leaseAcquiredAt??t,leaseDurationMs:e.leaseDurationMs??U,leaseExpiresAt:e.leaseExpiresAt??e.ttlExpiresAt},r=JSON.stringify(n,null,2);try{const s=await u.open(this.lockPath,"wx");try{await s.writeFile(r,"utf8")}finally{await s.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:n.pid,sessionId:n.sessionId},"yoloop lock acquired");return}catch(s){const i=s;if(i.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:i.message},"failed to acquire yoloop lock"),s;const a=await this.readLock();if(a&&this.isSameClientSessionLock(a,n)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:n.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(t))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),s;const l=await u.open(this.lockPath,"wx");try{await l.writeFile(r,"utf8")}finally{await l.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:n.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(e){const t=await this.readState(new Date().toISOString()),n=await this.readLock();if(t.runControl){if(!e.expectedRunId||!e.expectedSessionId||!e.expectedLeaseId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(e.expectedRunId!==t.runControl.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(!n||e.expectedSessionId!==n.sessionId||e.expectedLeaseId!==n.leaseId)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}try{await u.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(r){const s=r;if(s.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:s.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const e=await u.readFile(this.lockPath,"utf8"),t=JSON.parse(e),n=t.currentEpicId??t.epicId??"";return{pid:t.pid??0,hostname:t.hostname??"",...t.ownerKind?{ownerKind:t.ownerKind}:{},...t.ownerPidState?{ownerPidState:t.ownerPidState}:{},...typeof t.leaseId=="string"?{leaseId:t.leaseId}:{},...t.leaseState?{leaseState:t.leaseState}:{},...typeof t.leaseAcquiredAt=="string"?{leaseAcquiredAt:t.leaseAcquiredAt}:{},...typeof t.leaseDurationMs=="number"?{leaseDurationMs:t.leaseDurationMs}:{},...typeof t.leaseExpiresAt=="string"?{leaseExpiresAt:t.leaseExpiresAt}:{},startedAt:t.startedAt??"",sessionId:t.sessionId??"",mode:t.mode??"epic-id",inputPath:t.inputPath??"",epicQueue:t.epicQueue??(n?[n]:[]),currentEpicIndex:t.currentEpicIndex??0,currentEpicId:n,ttlExpiresAt:t.ttlExpiresAt??"",epicId:t.epicId??n}}catch{return null}}async readState(e){try{const t=await u.readFile(this.statePath,"utf8"),n=JSON.parse(t);return G(n,e)}catch(t){const n=t;return n.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:n.message},"yoloop state file unreadable; using defaults"),I(e)}}async writeState(e){const t=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,n=JSON.stringify(e,null,2);try{const r=await u.open(t,"w",384);try{await r.writeFile(n,"utf8"),await r.sync()}finally{await r.close()}this.trySetFilePerms(t),await u.rename(t,this.statePath),await this.syncStateDirectory()}catch(r){const s=r;this.logger.error({event:"yoloop_state_write_error",err:s.message},"failed to write yoloop state");try{await u.unlink(t)}catch{}throw r}}async advanceEpic(e,t,n){const r=await this.readState(n);if(!r.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:t},"advance_epic with no active session; skipping");return}const s={...r,currentSession:{...r.currentSession,currentEpicIndex:e}};await this.writeState(s),this.logger.info({event:"yoloop_epic_advance",nextEpicId:t,nextIndex:e},"yoloop advanced to next epic")}async ensureDir(){try{await u.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(e){const t=e;throw this.logger.error({event:"yoloop_mkdir_error",err:t.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),e}}async syncStateDirectory(){try{const e=await u.open(this.stateDir,"r");try{await e.sync()}finally{await e.close()}}catch(e){const t=e.code;if(t!=="EINVAL"&&t!=="EISDIR"&&t!=="ENOTSUP"&&t!=="EPERM")throw e}}async withMutationLock(e){let t=null;for(let n=0;n<b;n+=1)try{t=await u.open(this.mutationLockPath,"wx",384),await t.writeFile(JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()})),await t.sync();break}catch(r){if(r.code!=="EEXIST")throw r;try{const s=await u.stat(this.mutationLockPath);if(Date.now()-s.mtimeMs>K){await u.unlink(this.mutationLockPath);continue}}catch{continue}await new Promise(s=>setTimeout(s,5))}if(!t)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");try{return await e()}finally{await t.close().catch(()=>{}),await u.unlink(this.mutationLockPath).catch(()=>{})}}async transitionRun(e,t){$(e);const n=await this.readState(t),r=n.runControl,s=r?.receipts.find(l=>l.operationId===e.operationId||l.idempotencyKey===e.idempotencyKey);if(s){if(s.operationId===e.operationId&&s.idempotencyKey===e.idempotencyKey&&s.payloadHash===e.payloadHash)return s;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}if(r&&r.runId!==e.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((r?.revision??0)!==e.expectedRevision)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((r?.latestSnapshotHash??null)!==e.expectedSnapshotHash)throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT");B(r,e);const c=e.next.receipts.at(-1);if(!c)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");return await this.writeState({...n,version:"1.2",runControl:e.next}),this.logger.info({event:"yoloop_run_transition_committed",runId:e.runId,operationId:e.operationId,revision:c.revision,status:c.status},"yoloop run transition committed"),c}trySetFilePerms(e){try{k(e)}catch{}}trySetDirPerms(e){try{x(e)}catch{}}async prepareLockForSnapshot(e,t){if(!e)return null;if(e.ownerKind!=="client")return e;const n=N(e,t),r={...e,leaseState:n};if(n==="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(!r.pid||r.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(r.hostname&&r.hostname!==f())return{...r,ownerPidState:"unknown"};try{return process.kill(r.pid,0),{...r,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(e){const t=await this.readLock();if(!t)return!0;if(N(t,e)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!t.pid||t.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(t.hostname&&t.hostname!==f())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:t.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(t.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 u.unlink(this.lockPath)}catch{}}isSameClientSessionLock(e,t){return e.ownerKind==="client"&&e.sessionId===t.sessionId&&e.pid===process.pid&&e.hostname===f()}async cleanupOrphanTempFiles(){try{const e=C(this.statePath),t=await u.readdir(e),n=`${m}.tmp.`,r=t.filter(s=>s.startsWith(n));for(const s of r)try{await u.unlink(w(e,s))}catch{}}catch{}}}function h(o){return o===null||typeof o!="object"?JSON.stringify(o):Array.isArray(o)?`[${o.map(h).join(",")}]`:`{${Object.entries(o).filter(([,e])=>e!==void 0).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${h(t)}`).join(",")}}`}function y(o,e){return P("sha256").update(`${o}\0${h(e)}`).digest("hex")}function A(o){const{receipts:e,...t}=o.next;return y("p220-yoloop-run-transition-payload-v1",{runId:o.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,expectedRevision:o.expectedRevision,expectedSnapshotHash:o.expectedSnapshotHash,next:t})}const g=new Set(["success","partial","blocked","failure","exhausted","cancelled"]),q={created:["running","cancelling","exhausted"],running:["pausing","cancelling","success","partial","blocked","failure","exhausted"],pausing:["paused","cancelling","failure","exhausted"],paused:["resuming","cancelling","exhausted"],resuming:["running","cancelling","failure","exhausted"],cancelling:["cancelled","failure","exhausted"]};function M(o,e){return e==="pause"?o==="running"?"pausing":null:e==="resume"?o==="paused"?"resuming":null:g.has(o)||o==="cancelling"?null:"cancelling"}function V(o){const e=o.current.revision+1,t=o.action==="cancel"?["YOLOOP_RUN_CANCEL_REQUESTED"]:[],n={schemaVersion:"YoloopOperationalBudgetSummary/1.0",limits:o.current.operationalBudget.limits,usage:o.current.usage,exhaustedDimension:null},r={schemaVersion:"ProgressSnapshot/1.0",runId:o.current.runId,sequence:o.current.progressSnapshots.length+1,revision:e,operationId:o.operationId,boundary:o.action,phase:"before",status:o.current.status,occurredAt:o.occurredAt,parentSnapshotHash:o.current.latestSnapshotHash,budget:n,reasonCodes:t,publicSafe:!0},s=y("p220-yoloop-progress-snapshot-id-v1",{runId:r.runId,operationId:r.operationId,sequence:r.sequence,phase:r.phase}),i={...r,snapshotId:s,snapshotHash:y("p220-yoloop-progress-snapshot-v1",{...r,snapshotId:s})},a={...r,sequence:i.sequence+1,phase:"after",status:o.target,parentSnapshotHash:i.snapshotHash},c=y("p220-yoloop-progress-snapshot-id-v1",{runId:a.runId,operationId:a.operationId,sequence:a.sequence,phase:a.phase}),l={...a,snapshotId:c,snapshotHash:y("p220-yoloop-progress-snapshot-v1",{...a,snapshotId:c})},d={schemaVersion:"YoloopRunEvent/1.0",runId:o.current.runId,operationId:o.operationId,kind:"intent",sequence:o.current.events.length+1,revision:e,occurredAt:o.occurredAt,fromStatus:o.current.status,toStatus:o.target,reasonCodes:t,budgetDimension:null,parentEventHash:o.current.events.at(-1)?.eventHash??null,publicSafe:!0},p=y("p220-yoloop-run-event-id-v1",{runId:d.runId,operationId:d.operationId,sequence:d.sequence}),_={...d,eventId:p,eventHash:y("p220-yoloop-run-event-v1",{...d,eventId:p})},O={...o.current,revision:e,status:o.target,events:[...o.current.events,_],progressSnapshots:[...o.current.progressSnapshots,i,l],latestSnapshotHash:l.snapshotHash,receipts:[...o.current.receipts]},v={runId:o.current.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,expectedRevision:o.current.revision,expectedSnapshotHash:o.current.latestSnapshotHash,next:O},S=A(v),L={schemaVersion:"YoloopRunTransitionReceipt/1.0",runId:o.current.runId,operationId:o.operationId,idempotencyKey:o.idempotencyKey,payloadHash:S,revision:e,snapshotHash:l.snapshotHash,status:o.target};return{type:"transition_run",...v,payloadHash:S,next:{...O,receipts:[...O.receipts,L]}}}function $(o){const e=/^[A-Za-z0-9._:-]{1,128}$/;if(!e.test(o.runId)||!e.test(o.operationId)||!e.test(o.idempotencyKey)||!/^[a-f0-9]{64}$/.test(o.payloadHash)||!Number.isSafeInteger(o.expectedRevision)||o.expectedRevision<0)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");if(o.payloadHash!==A(o))throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}function B(o,e){const t=e.next,n=o?.events??[],r=o?.progressSnapshots??[],s=o?.receipts??[];if(t.schemaVersion!=="YoloopRunControl/1.0"||t.runId!==e.runId||t.revision!==e.expectedRevision+1||t.events.length!==n.length+1||t.progressSnapshots.length!==r.length+2||t.receipts.length!==s.length+1||h(t.events.slice(0,n.length))!==h(n)||h(t.progressSnapshots.slice(0,r.length))!==h(r)||h(t.receipts.slice(0,s.length))!==h(s))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(o){if(g.has(o.status))throw new Error("YOLOOP_RUN_TERMINAL");const d=t.progressSnapshots.at(-2)?.boundary,p=o.status===t.status&&(d==="budget"||d==="retry");if(!(q[o.status]??[]).includes(t.status)&&!p)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(o.startedAt!==t.startedAt||o.deadlineAt!==t.deadlineAt)throw new Error("YOLOOP_RUN_TRANSITION_INVALID")}else if(t.status!=="created")throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const i=t.progressSnapshots.at(-2),a=t.progressSnapshots.at(-1),c=t.events.at(-1),l=t.receipts.at(-1);if(!i||!a||!c||!l||i.phase!=="before"||a.phase!=="after"||i.operationId!==e.operationId||a.operationId!==e.operationId||c.operationId!==e.operationId||i.runId!==e.runId||a.runId!==e.runId||c.runId!==e.runId||i.revision!==t.revision||a.revision!==t.revision||c.revision!==t.revision||i.parentSnapshotHash!==(o?.latestSnapshotHash??null)||a.parentSnapshotHash!==i.snapshotHash||t.latestSnapshotHash!==a.snapshotHash||l.schemaVersion!=="YoloopRunTransitionReceipt/1.0"||l.runId!==e.runId||l.operationId!==e.operationId||l.idempotencyKey!==e.idempotencyKey||l.payloadHash!==e.payloadHash||l.revision!==t.revision||l.snapshotHash!==t.latestSnapshotHash||l.status!==t.status||Date.parse(t.deadlineAt)<=Date.parse(t.startedAt))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");E(i),E(a),J(c,n.at(-1)?.eventHash??null)}function E(o){const{snapshotHash:e,...t}=o;if(e!==y("p220-yoloop-progress-snapshot-v1",t))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function J(o,e){const{eventHash:t,...n}=o;if(o.parentEventHash!==e||t!==y("p220-yoloop-run-event-v1",n))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function j(o,e){return`lease-${W(`${o}:${e}`)}`}function W(o){let e=2166136261;for(let t=0;t<o.length;t+=1)e^=o.charCodeAt(t),e=Math.imul(e,16777619)>>>0;return e.toString(36).padStart(7,"0")}function N(o,e){const t=o.leaseExpiresAt??o.ttlExpiresAt,n=Date.parse(t),r=Date.parse(e);return!Number.isFinite(n)||!Number.isFinite(r)?"unknown":n<=r?"expired":"active"}function G(o,e){const t=I(e);return{...o,version:o.version==="1.2"?"1.2":"1.1",lastResetAt:o.lastResetAt??t.lastResetAt,storiesRunToday:o.storiesRunToday??0,iterationsToday:o.iterationsToday??0,iterationsTodayDate:o.iterationsTodayDate??t.iterationsTodayDate,tokensUsedToday:o.tokensUsedToday??0,lastRunAt:o.lastRunAt??null,coolDownUntil:o.coolDownUntil??null,totalRunsAllTime:o.totalRunsAllTime??0,totalStoriesAllTime:o.totalStoriesAllTime??0,currentSession:o.currentSession??null,...o.runControl!==void 0?{runControl:o.runControl}:{},...o.continuationPolicy!==void 0?{continuationPolicy:o.continuationPolicy}:{},...o.continuationSession!==void 0?{continuationSession:o.continuationSession}:{},...o.pendingContinuationAuthorities!==void 0?{pendingContinuationAuthorities:o.pendingContinuationAuthorities}:{},...o.continuationReceipts!==void 0?{continuationReceipts:o.continuationReceipts}:{},...o.continuationRecoveryReceipts!==void 0?{continuationRecoveryReceipts:o.continuationRecoveryReceipts}:{},recovery_history:Array.isArray(o.recovery_history)?T(o.recovery_history):[]}}function T(o){return o.length<=R?o:o.slice(o.length-R)}export{ne as YoloopClientStateStore,A as buildYoloopRunTransitionPayloadHash,T as capRecoveryHistory,I as createDefaultCounters,oe as createDefaultState};
|
|
1
|
+
import{promises as u}from"node:fs";import{createHash as Y}from"node:crypto";import{join as _,dirname as U}from"node:path";import{hostname as I}from"node:os";import{setSecureFilePermissions as F,setSecureDirPermissions as K}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as g}from"./shared-yoloop-types.js";const b=".neocortex",m="yoloop-state.json",V="yoloop.lock",M="yoloop-state.mutation.lock",T="yoloop-session-rollover.json",q=360*60*1e3,$=3e4,B=200,C=Object.freeze(["rollover-session-v1"]),J={warn:()=>{},info:()=>{},error:()=>{}};function S(t){const e=t.slice(0,10);return{version:"1.1",lastResetAt:t,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:e,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null,continuationPolicy:"p205-finite-resume-v2",pendingContinuationAuthorities:[],continuationReceipts:[],continuationRecoveryReceipts:[]}}function ue(t){return{lock:null,state:S(t),capabilities:C}}class he{stateDir;statePath;lockPath;mutationLockPath;rolloverManifestPath;logger;constructor(e,n=J){this.stateDir=_(e,b),this.statePath=_(this.stateDir,m),this.lockPath=_(this.stateDir,V),this.mutationLockPath=_(this.stateDir,M),this.rolloverManifestPath=_(this.stateDir,T),this.logger=n}async readAll(e){const n=e??new Date().toISOString();await this.cleanupOrphanTempFiles(),await this.recoverSessionRolloverIfPresent(n);const[s,o]=await Promise.all([this.readLock(),this.readState(n)]);return{lock:await this.prepareLockForSnapshot(s,n),state:o,capabilities:C}}async applyUpdate(e,n){const s=n??new Date().toISOString();await this.ensureDir(),await this.recoverSessionRolloverIfPresent(s);for(const o of e.operations)await this.applyOperation(o,s)}async requestRunTransition(e,n){const s=n??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(()=>this.transitionRun(e,s))}async requestLifecycleAction(e,n={},s){const o=s??new Date().toISOString();return await this.ensureDir(),this.withMutationLock(async()=>{const i=(await this.readState(o)).runControl;if(!i)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const a=Date.parse(o).toString(36),c=n.operationId??`op-${e}-${a}`,l=n.idempotencyKey??`idem-${e}-${a}`,h=i.receipts.find(y=>y.operationId===c||y.idempotencyKey===l);if(h){if(h.operationId===c&&h.idempotencyKey===l)return h;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}const O=Z(i.status,e);if(!O)throw new Error(R.has(i.status)?"YOLOOP_RUN_TERMINAL":"YOLOOP_RUN_TRANSITION_INVALID");return this.transitionRun(ee({current:i,action:e,target:O,operationId:c,idempotencyKey:l,occurredAt:o}),o)})}async applyOperation(e,n){switch(e.type){case"acquire_lock":await this.acquireLock(e.lock,n);return;case"release_lock":await this.releaseLock(e);return;case"write_state":await this.withMutationLock(async()=>{const s=await this.readState(n),{runControl:o,...r}=e.state;await this.writeState({...r,...s.runControl?{version:"1.2",runControl:s.runControl}:{}})});return;case"advance_epic":await this.advanceEpic(e.nextEpicIndex,e.nextEpicId,n);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"reserve_continuation_authority":await this.reserveContinuationAuthority(e.authority,n);return;case"acknowledge_continuation_authority":await this.acknowledgeContinuationAuthority(e.idempotencyKey,e.receipt,n);return;case"repair_continuation_authority":await this.repairContinuationAuthority(e.beforeHash,e.authority,e.receipt,n);return;case"switch_continuation_policy":await this.switchContinuationPolicy(e.policy,e.session,n);return;case"transition_run":await this.requestRunTransition(e,n);return;case"start_successor_run":await this.withMutationLock(()=>this.startSuccessorRun(e,n));return;case"rollover_session":await this.withMutationLock(()=>this.rolloverSession(e,n));return;case"recovery_attempted":await this.appendRecoveryAttempt(e.attempt,n);return;case"recovery_resolved":await this.resolveRecoveryAttempt(e.attempt_id,e.outcome,n);return;default:{const s=e;throw this.logger.error({event:"yoloop_unknown_op",op:s},"unknown yoloop operation rejected"),new Error("YOLOOP_STATE_OPERATION_UNSUPPORTED")}}}async reserveContinuationAuthority(e,n){const s=await this.readState(n),o=s.pendingContinuationAuthorities??[],r=typeof e.idempotencyKey=="string"?e.idempotencyKey:"";if(!r)throw new Error("YOLOOP_CONTINUATION_AUTHORITY_INVALID");const i=o.find(a=>a.idempotencyKey===r);if(i){if(d(i)===d(e))return;throw new Error("YOLOOP_CONTINUATION_AUTHORITY_CONFLICT")}if(o.length>=64)throw new Error("YOLOOP_PENDING_AUTHORITY_CAPACITY_EXCEEDED");await this.writeState({...s,pendingContinuationAuthorities:[...o,e]})}async acknowledgeContinuationAuthority(e,n,s){const o=await this.readState(s),r=o.pendingContinuationAuthorities??[],i=r.findIndex(h=>h.idempotencyKey===e);if(i<0)throw new Error("YOLOOP_CONTINUATION_RECEIPT_ORPHAN");const a=r[i];if(n.idempotencyKey!==e||n.sessionId!==a.sessionId||JSON.stringify(n.storyIds)!==JSON.stringify(a.storyIds))throw new Error("YOLOOP_CONTINUATION_RECEIPT_MISMATCH");const c=r.filter((h,O)=>O!==i),l=o.continuationReceipts??[];await this.writeState({...o,pendingContinuationAuthorities:c,continuationReceipts:[...l.slice(-63),n]})}async repairContinuationAuthority(e,n,s,o){const r=await this.readState(o),i=r.pendingContinuationAuthorities??[],a=r.continuationRecoveryReceipts??[],c=typeof s.idempotencyKey=="string"?s.idempotencyKey:"",l=a.find(y=>y.idempotencyKey===c);if(l){if(d(l)===d(s))return;throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_CONFLICT")}if(i.length!==1||p("p208-legacy-authority-before-v1",i[0])!==e)throw new Error("YOLOOP_CONTINUATION_RECOVERY_BEFORE_MISMATCH");const h=p("p208-legacy-authority-after-v1",n);if(s.beforeHash!==e||s.afterHash!==h)throw new Error("YOLOOP_CONTINUATION_RECOVERY_HASH_MISMATCH");const O={...s};if(delete O.idempotencyKey,!c||p("p208-legacy-authority-recovery-receipt-v1",O)!==c)throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_INVALID");await this.writeState({...r,pendingContinuationAuthorities:[n],continuationRecoveryReceipts:[...a.slice(-63),s]})}async switchContinuationPolicy(e,n,s){const o=await this.readState(s);if(o.currentSession||(o.pendingContinuationAuthorities?.length??0)>0)throw new Error("YOLOOP_POLICY_SWITCH_ACTIVE");try{await u.copyFile(this.statePath,`${this.statePath}.policy-backup`)}catch(r){if(r.code!=="ENOENT")throw r}await this.writeState({...o,continuationPolicy:e,continuationSession:n})}async appendRecoveryAttempt(e,n){const s=await this.readState(n),o=s.recovery_history??[];if(o.some(c=>c.attempted_at===e.attempted_at&&c.story_id===e.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:e.story_id},"duplicate recovery_attempted op; skipping");return}const i=H([...o,e]),a={...s,recovery_history:i};await this.writeState(a),this.logger.info({event:"recovery_attempted_persisted",story_id:e.story_id,history_size:i.length},"recovery attempt persisted")}async resolveRecoveryAttempt(e,n,s){const o=await this.readState(s),r=o.recovery_history??[],i=r.findIndex(h=>h.recovery_step_id_tokenized===e);if(i===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:e},"recovery_resolved op had no matching attempt; skipping");return}const a=r[i];if(a.outcome===n&&a.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:e,outcome:n},"duplicate recovery_resolved op; skipping");return}const c=r.slice();c[i]={...a,outcome:n,finished_at:s};const l={...o,recovery_history:c};await this.writeState(l),this.logger.info({event:"recovery_resolved_persisted",attempt_id:e,outcome:n},"recovery attempt resolved")}async acquireLock(e,n){const s={...e,pid:process.pid,hostname:I(),ownerKind:"client",startedAt:e.startedAt||n,leaseId:e.leaseId??ne(e.sessionId,n),leaseState:"active",leaseAcquiredAt:e.leaseAcquiredAt??n,leaseDurationMs:e.leaseDurationMs??q,leaseExpiresAt:e.leaseExpiresAt??e.ttlExpiresAt},o=JSON.stringify(s,null,2);try{const r=await u.open(this.lockPath,"wx");try{await r.writeFile(o,"utf8")}finally{await r.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:s.pid,sessionId:s.sessionId},"yoloop lock acquired");return}catch(r){const i=r;if(i.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:i.message},"failed to acquire yoloop lock"),r;const a=await this.readLock();if(a&&this.isSameClientSessionLock(a,s)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:s.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(n))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),r;const l=await u.open(this.lockPath,"wx");try{await l.writeFile(o,"utf8")}finally{await l.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:s.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(e){const n=await this.readState(new Date().toISOString()),s=await this.readLock();if(n.runControl){if(!e.expectedRunId||!e.expectedSessionId||!e.expectedLeaseId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(e.expectedRunId!==n.runControl.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(!s||e.expectedSessionId!==s.sessionId||e.expectedLeaseId!==s.leaseId)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}try{await u.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(o){const r=o;if(r.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:r.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const e=await u.readFile(this.lockPath,"utf8"),n=JSON.parse(e),s=n.currentEpicId??n.epicId??"";return{pid:n.pid??0,hostname:n.hostname??"",...n.ownerKind?{ownerKind:n.ownerKind}:{},...n.ownerPidState?{ownerPidState:n.ownerPidState}:{},...typeof n.leaseId=="string"?{leaseId:n.leaseId}:{},...n.leaseState?{leaseState:n.leaseState}:{},...typeof n.leaseAcquiredAt=="string"?{leaseAcquiredAt:n.leaseAcquiredAt}:{},...typeof n.leaseDurationMs=="number"?{leaseDurationMs:n.leaseDurationMs}:{},...typeof n.leaseExpiresAt=="string"?{leaseExpiresAt:n.leaseExpiresAt}:{},startedAt:n.startedAt??"",sessionId:n.sessionId??"",mode:n.mode??"epic-id",inputPath:n.inputPath??"",epicQueue:n.epicQueue??(s?[s]:[]),currentEpicIndex:n.currentEpicIndex??0,currentEpicId:s,ttlExpiresAt:n.ttlExpiresAt??"",epicId:n.epicId??s}}catch{return null}}async readState(e){try{const n=await u.readFile(this.statePath,"utf8"),s=JSON.parse(n);return se(s,e)}catch(n){const s=n;return s.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:s.message},"yoloop state file unreadable; using defaults"),S(e)}}async writeState(e){const n=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,s=JSON.stringify(e,null,2);try{const o=await u.open(n,"w",384);try{await o.writeFile(s,"utf8"),await o.sync()}finally{await o.close()}this.trySetFilePerms(n),await u.rename(n,this.statePath),await this.syncStateDirectory()}catch(o){const r=o;this.logger.error({event:"yoloop_state_write_error",err:r.message},"failed to write yoloop state");try{await u.unlink(n)}catch{}throw o}}async rolloverSession(e,n){f(e);const s=await this.readRolloverManifest();if(s){if(G(s.operation,e)){if(s.operation.payloadHash!==e.payloadHash)throw new Error("YOLOOP_SESSION_ROLLOVER_CONFLICT");if(s.phase==="applied")return;await this.completePreparedSessionRollover(s.operation,n);return}else if(s.phase==="prepared")throw new Error("YOLOOP_SESSION_ROLLOVER_CONFLICT")}const o=await this.readState(n),r=await this.readLock();P(e,o,r),await this.writeRolloverManifest({schemaVersion:"YoloopSessionRolloverManifest/1.0",phase:"prepared",operation:e}),await this.completePreparedSessionRollover(e,n)}async completePreparedSessionRollover(e,n){const s=await this.readState(n);let o=d(s)===d(e.next);if(!o){const i=await this.readLock();P(e,s,i),await this.writeState(e.next),o=!0}const r=await this.readLock();if(e.expectedSessionId===null||e.expectedLeaseId===null){if(e.expectedSessionId!==null||e.expectedLeaseId!==null||r)throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT")}else if(r){if(r.sessionId!==e.expectedSessionId||r.leaseId!==e.expectedLeaseId)throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT");await u.unlink(this.lockPath),await this.syncStateDirectory()}else if(!o)throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT");await this.writeRolloverManifest({schemaVersion:"YoloopSessionRolloverManifest/1.0",phase:"applied",operation:e}),this.logger.info({event:"yoloop_session_rollover_applied",runId:e.runId,operationId:e.operationId,coolDownUntil:e.coolDownUntil},"yoloop per-run-cap session rollover applied")}async recoverSessionRolloverIfPresent(e){try{await u.access(this.rolloverManifestPath)}catch(n){if(n.code==="ENOENT")return;throw n}await this.ensureDir(),await this.withMutationLock(async()=>{const n=await this.readRolloverManifest();n?.phase==="prepared"&&await this.completePreparedSessionRollover(n.operation,e)})}async readRolloverManifest(){try{const e=JSON.parse(await u.readFile(this.rolloverManifestPath,"utf8"));if(e.schemaVersion!=="YoloopSessionRolloverManifest/1.0"||e.phase!=="prepared"&&e.phase!=="applied"||!e.operation||e.operation.type!=="rollover_session")throw new Error("YOLOOP_SESSION_ROLLOVER_MANIFEST_CORRUPT");return f(e.operation),e}catch(e){if(e.code==="ENOENT")return null;throw e.message==="YOLOOP_SESSION_ROLLOVER_MANIFEST_CORRUPT"?e:new Error("YOLOOP_SESSION_ROLLOVER_MANIFEST_CORRUPT")}}async writeRolloverManifest(e){const n=`${this.rolloverManifestPath}.tmp.${process.pid}.${Date.now()}`,s=await u.open(n,"w",384);try{await s.writeFile(JSON.stringify(e,null,2),"utf8"),await s.sync()}finally{await s.close()}this.trySetFilePerms(n),await u.rename(n,this.rolloverManifestPath),await this.syncStateDirectory()}async advanceEpic(e,n,s){const o=await this.readState(s);if(!o.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:n},"advance_epic with no active session; skipping");return}const r={...o,currentSession:{...o.currentSession,currentEpicIndex:e}};await this.writeState(r),this.logger.info({event:"yoloop_epic_advance",nextEpicId:n,nextIndex:e},"yoloop advanced to next epic")}async ensureDir(){try{await u.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(e){const n=e;throw this.logger.error({event:"yoloop_mkdir_error",err:n.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),e}}async syncStateDirectory(){try{const e=await u.open(this.stateDir,"r");try{await e.sync()}finally{await e.close()}}catch(e){const n=e.code;if(n!=="EINVAL"&&n!=="EISDIR"&&n!=="ENOTSUP"&&n!=="EPERM")throw e}}async withMutationLock(e){let n=null;for(let s=0;s<B;s+=1)try{n=await u.open(this.mutationLockPath,"wx",384),await n.writeFile(JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()})),await n.sync();break}catch(o){if(o.code!=="EEXIST")throw o;try{const r=await u.stat(this.mutationLockPath);if(Date.now()-r.mtimeMs>$){await u.unlink(this.mutationLockPath);continue}}catch{continue}await new Promise(r=>setTimeout(r,5))}if(!n)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");try{return await e()}finally{await n.close().catch(()=>{}),await u.unlink(this.mutationLockPath).catch(()=>{})}}async transitionRun(e,n){v(e);const s=await this.readState(n),o=s.runControl,r=o?.receipts.find(l=>l.operationId===e.operationId||l.idempotencyKey===e.idempotencyKey);if(r){if(r.operationId===e.operationId&&r.idempotencyKey===e.idempotencyKey&&r.payloadHash===e.payloadHash)return r;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}if(o&&o.runId!==e.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((o?.revision??0)!==e.expectedRevision)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if((o?.latestSnapshotHash??null)!==e.expectedSnapshotHash)throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT");E(o,e);const c=e.next.receipts.at(-1);if(!c)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");return await this.writeState({...s,version:"1.2",runControl:e.next}),this.logger.info({event:"yoloop_run_transition_committed",runId:e.runId,operationId:e.operationId,revision:c.revision,status:c.status},"yoloop run transition committed"),c}async startSuccessorRun(e,n){X(e);const s=await this.readState(n),o=s.runControl,r={schemaVersion:"YoloopRunPredecessor/1.0",runId:e.previousRunId,revision:e.expectedRevision,snapshotHash:e.expectedSnapshotHash,successorOperationId:e.operationId,publicSafe:!0},i=o?.receipts.find(l=>l.operationId===e.operationId||l.idempotencyKey===e.idempotencyKey);if(i){if(i.operationId===e.operationId&&i.idempotencyKey===e.idempotencyKey&&e.next.receipts.at(-1)?.payloadHash===i.payloadHash&&d(o?.predecessor)===d(r))return i;throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}if(!o||o.runId!==e.previousRunId||o.status!=="exhausted"||o.revision!==e.expectedRevision||o.latestSnapshotHash!==e.expectedSnapshotHash||s.currentSession!==null||!s.coolDownUntil||Date.parse(s.coolDownUntil)>Date.parse(n))throw new Error("YOLOOP_RUN_SUCCESSOR_CONFLICT");const a=e.next.receipts.at(-1);if(!a||d(e.next.predecessor)!==d(r))throw new Error("YOLOOP_RUN_SUCCESSOR_CONFLICT");const c={type:"transition_run",runId:e.runId,operationId:e.operationId,idempotencyKey:e.idempotencyKey,expectedRevision:0,expectedSnapshotHash:null,payloadHash:a.payloadHash,next:e.next};return v(c),E(void 0,c),await this.writeState({...s,version:"1.2",runControl:e.next}),a}trySetFilePerms(e){try{F(e)}catch{}}trySetDirPerms(e){try{K(e)}catch{}}async prepareLockForSnapshot(e,n){if(!e)return null;if(e.ownerKind!=="client")return e;const s=k(e,n),o={...e,leaseState:s};if(s==="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!==I())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(e){const n=await this.readLock();if(!n)return!0;if(k(n,e)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!n.pid||n.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(n.hostname&&n.hostname!==I())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:n.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(n.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 u.unlink(this.lockPath)}catch{}}isSameClientSessionLock(e,n){return e.ownerKind==="client"&&e.sessionId===n.sessionId&&e.pid===process.pid&&e.hostname===I()}async cleanupOrphanTempFiles(){try{const e=U(this.statePath),n=await u.readdir(e),s=[`${m}.tmp.`,`${T}.tmp.`],o=n.filter(r=>s.some(i=>r.startsWith(i)));for(const r of o)try{await u.unlink(_(e,r))}catch{}}catch{}}}function d(t){return t===null||typeof t!="object"?JSON.stringify(t):Array.isArray(t)?`[${t.map(d).join(",")}]`:`{${Object.entries(t).filter(([,e])=>e!==void 0).sort(([e],[n])=>e.localeCompare(n)).map(([e,n])=>`${JSON.stringify(e)}:${d(n)}`).join(",")}}`}function p(t,e){return Y("sha256").update(`${t}\0${d(e)}`).digest("hex")}function j(t){return p("p9390812898200217364-yoloop-session-v1",t)}function z(t){return p("p9390812898200217364-yoloop-rollover-v1",t)}function W(t){return p("p9390812898200217364-yoloop-successor-run-v1",t)}function G(t,e){return t.operationId===e.operationId&&t.idempotencyKey===e.idempotencyKey}function f(t){const e=/^[A-Za-z0-9._:-]{1,128}$/,n={runId:t.runId,operationId:t.operationId,idempotencyKey:t.idempotencyKey,expectedRevision:t.expectedRevision,expectedSnapshotHash:t.expectedSnapshotHash,expectedSessionId:t.expectedSessionId,expectedSessionFingerprint:t.expectedSessionFingerprint,expectedLeaseId:t.expectedLeaseId,coolDownUntil:t.coolDownUntil,reason:t.reason,exhaustedRun:t.exhaustedRun,next:t.next};if(!e.test(t.runId)||!e.test(t.operationId)||!e.test(t.idempotencyKey)||!/^[a-f0-9]{64}$/.test(t.expectedSessionFingerprint)||!/^[a-f0-9]{64}$/.test(t.payloadHash)||!Number.isSafeInteger(t.expectedRevision)||t.expectedRevision<0||!Number.isFinite(Date.parse(t.coolDownUntil))||!["YOLOOP_PER_RUN_CAP_REACHED","YOLOOP_SESSION_TIMEOUT"].includes(t.reason)||t.payloadHash!==z(n)||t.receipt.schemaVersion!=="YoloopSessionRolloverReceipt/1.0"||t.receipt.runId!==t.runId||t.receipt.operationId!==t.operationId||t.receipt.idempotencyKey!==t.idempotencyKey||t.receipt.payloadHash!==t.payloadHash||t.receipt.coolDownUntil!==t.coolDownUntil||t.receipt.revision!==(t.exhaustedRun?.revision??0)||t.receipt.snapshotHash!==(t.exhaustedRun?.latestSnapshotHash??null)||t.receipt.status!=="exhausted"||t.receipt.publicSafe!==!0)throw new Error("YOLOOP_SESSION_ROLLOVER_CONFLICT");if(t.expectedSessionId===null!=(t.expectedLeaseId===null)||t.expectedSessionId!==null&&!e.test(t.expectedSessionId)||t.expectedLeaseId!==null&&!e.test(t.expectedLeaseId))throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT")}function X(t){const e=/^[A-Za-z0-9._:-]{1,128}$/,n={runId:t.runId,previousRunId:t.previousRunId,operationId:t.operationId,idempotencyKey:t.idempotencyKey,expectedRevision:t.expectedRevision,expectedSnapshotHash:t.expectedSnapshotHash,next:t.next};if(!e.test(t.runId)||!e.test(t.previousRunId)||t.runId===t.previousRunId||!e.test(t.operationId)||!e.test(t.idempotencyKey)||!/^[a-f0-9]{64}$/.test(t.payloadHash)||!Number.isSafeInteger(t.expectedRevision)||t.expectedRevision<1||t.payloadHash!==W(n))throw new Error("YOLOOP_RUN_SUCCESSOR_CONFLICT")}function P(t,e,n){f(t);const s=e.currentSession;if(!s||j(s)!==t.expectedSessionFingerprint)throw new Error("YOLOOP_SESSION_ROLLOVER_SESSION_CONFLICT");const o=e.runControl;if((o?.revision??0)!==t.expectedRevision||(o?.latestSnapshotHash??null)!==t.expectedSnapshotHash||o&&o.runId!==t.runId)throw new Error("YOLOOP_RUN_REVISION_CONFLICT");if(t.expectedSessionId===null||t.expectedLeaseId===null){if(n)throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT")}else if(!n||n.sessionId!==t.expectedSessionId||n.leaseId!==t.expectedLeaseId)throw new Error("YOLOOP_SESSION_ROLLOVER_LOCK_CONFLICT");const r=t.exhaustedRun;if(o)if(o.status==="exhausted"){if(d(r)!==d(o))throw new Error("YOLOOP_SESSION_ROLLOVER_STATE_CONFLICT")}else{if(!r||r.status!=="exhausted")throw new Error("YOLOOP_SESSION_ROLLOVER_STATE_CONFLICT");const a=r.receipts.at(-1);if(!a)throw new Error("YOLOOP_SESSION_ROLLOVER_STATE_CONFLICT");const c={type:"transition_run",runId:o.runId,operationId:a.operationId,idempotencyKey:a.idempotencyKey,expectedRevision:o.revision,expectedSnapshotHash:o.latestSnapshotHash,payloadHash:a.payloadHash,next:r};v(c),E(o,c)}else if(r)throw new Error("YOLOOP_SESSION_ROLLOVER_STATE_CONFLICT");const i={...e,version:"1.2",coolDownUntil:t.coolDownUntil,currentSession:null,...r?{runControl:r}:{}};if(d(i)!==d(t.next))throw new Error("YOLOOP_SESSION_ROLLOVER_STATE_CONFLICT")}function A(t){const{receipts:e,...n}=t.next;return p("p220-yoloop-run-transition-payload-v1",{runId:t.runId,operationId:t.operationId,idempotencyKey:t.idempotencyKey,expectedRevision:t.expectedRevision,expectedSnapshotHash:t.expectedSnapshotHash,next:n})}const R=new Set(["success","partial","blocked","failure","exhausted","cancelled"]),Q={created:["running","cancelling","exhausted"],running:["pausing","cancelling","success","partial","blocked","failure","exhausted"],pausing:["paused","cancelling","failure","exhausted"],paused:["resuming","cancelling","exhausted"],resuming:["running","cancelling","failure","exhausted"],cancelling:["cancelled","failure","exhausted"]};function Z(t,e){return e==="pause"?t==="running"?"pausing":null:e==="resume"?t==="paused"?"resuming":null:R.has(t)||t==="cancelling"?null:"cancelling"}function ee(t){const e=t.current.revision+1,n=t.action==="cancel"?["YOLOOP_RUN_CANCEL_REQUESTED"]:[],s={schemaVersion:"YoloopOperationalBudgetSummary/1.0",limits:t.current.operationalBudget.limits,usage:t.current.usage,exhaustedDimension:null},o={schemaVersion:"ProgressSnapshot/1.0",runId:t.current.runId,sequence:t.current.progressSnapshots.length+1,revision:e,operationId:t.operationId,boundary:t.action,phase:"before",status:t.current.status,occurredAt:t.occurredAt,parentSnapshotHash:t.current.latestSnapshotHash,budget:s,reasonCodes:n,publicSafe:!0},r=p("p220-yoloop-progress-snapshot-id-v1",{runId:o.runId,operationId:o.operationId,sequence:o.sequence,phase:o.phase}),i={...o,snapshotId:r,snapshotHash:p("p220-yoloop-progress-snapshot-v1",{...o,snapshotId:r})},a={...o,sequence:i.sequence+1,phase:"after",status:t.target,parentSnapshotHash:i.snapshotHash},c=p("p220-yoloop-progress-snapshot-id-v1",{runId:a.runId,operationId:a.operationId,sequence:a.sequence,phase:a.phase}),l={...a,snapshotId:c,snapshotHash:p("p220-yoloop-progress-snapshot-v1",{...a,snapshotId:c})},h={schemaVersion:"YoloopRunEvent/1.0",runId:t.current.runId,operationId:t.operationId,kind:"intent",sequence:t.current.events.length+1,revision:e,occurredAt:t.occurredAt,fromStatus:t.current.status,toStatus:t.target,reasonCodes:n,budgetDimension:null,parentEventHash:t.current.events.at(-1)?.eventHash??null,publicSafe:!0},O=p("p220-yoloop-run-event-id-v1",{runId:h.runId,operationId:h.operationId,sequence:h.sequence}),y={...h,eventId:O,eventHash:p("p220-yoloop-run-event-v1",{...h,eventId:O})},w={...t.current,revision:e,status:t.target,events:[...t.current.events,y],progressSnapshots:[...t.current.progressSnapshots,i,l],latestSnapshotHash:l.snapshotHash,receipts:[...t.current.receipts]},L={runId:t.current.runId,operationId:t.operationId,idempotencyKey:t.idempotencyKey,expectedRevision:t.current.revision,expectedSnapshotHash:t.current.latestSnapshotHash,next:w},N=A(L),D={schemaVersion:"YoloopRunTransitionReceipt/1.0",runId:t.current.runId,operationId:t.operationId,idempotencyKey:t.idempotencyKey,payloadHash:N,revision:e,snapshotHash:l.snapshotHash,status:t.target};return{type:"transition_run",...L,payloadHash:N,next:{...w,receipts:[...w.receipts,D]}}}function v(t){const e=/^[A-Za-z0-9._:-]{1,128}$/;if(!e.test(t.runId)||!e.test(t.operationId)||!e.test(t.idempotencyKey)||!/^[a-f0-9]{64}$/.test(t.payloadHash)||!Number.isSafeInteger(t.expectedRevision)||t.expectedRevision<0)throw new Error("YOLOOP_RUN_OPERATION_CONFLICT");if(t.payloadHash!==A(t))throw new Error("YOLOOP_RUN_OPERATION_CONFLICT")}function E(t,e){const n=e.next,s=t?.events??[],o=t?.progressSnapshots??[],r=t?.receipts??[];if(n.schemaVersion!=="YoloopRunControl/1.0"||n.runId!==e.runId||n.revision!==e.expectedRevision+1||n.events.length!==s.length+1||n.progressSnapshots.length!==o.length+2||n.receipts.length!==r.length+1||d(n.events.slice(0,s.length))!==d(s)||d(n.progressSnapshots.slice(0,o.length))!==d(o)||d(n.receipts.slice(0,r.length))!==d(r))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(t){if(R.has(t.status))throw new Error("YOLOOP_RUN_TERMINAL");const h=n.progressSnapshots.at(-2)?.boundary,O=t.status===n.status&&(h==="budget"||h==="retry");if(!(Q[t.status]??[]).includes(n.status)&&!O)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(t.startedAt!==n.startedAt||t.deadlineAt!==n.deadlineAt)throw new Error("YOLOOP_RUN_TRANSITION_INVALID");if(d(t.predecessor)!==d(n.predecessor))throw new Error("YOLOOP_RUN_TRANSITION_INVALID")}else if(n.status!=="created")throw new Error("YOLOOP_RUN_TRANSITION_INVALID");const i=n.progressSnapshots.at(-2),a=n.progressSnapshots.at(-1),c=n.events.at(-1),l=n.receipts.at(-1);if(!i||!a||!c||!l||i.phase!=="before"||a.phase!=="after"||i.operationId!==e.operationId||a.operationId!==e.operationId||c.operationId!==e.operationId||i.runId!==e.runId||a.runId!==e.runId||c.runId!==e.runId||i.revision!==n.revision||a.revision!==n.revision||c.revision!==n.revision||i.parentSnapshotHash!==(t?.latestSnapshotHash??null)||a.parentSnapshotHash!==i.snapshotHash||n.latestSnapshotHash!==a.snapshotHash||l.schemaVersion!=="YoloopRunTransitionReceipt/1.0"||l.runId!==e.runId||l.operationId!==e.operationId||l.idempotencyKey!==e.idempotencyKey||l.payloadHash!==e.payloadHash||l.revision!==n.revision||l.snapshotHash!==n.latestSnapshotHash||l.status!==n.status||Date.parse(n.deadlineAt)<=Date.parse(n.startedAt))throw new Error("YOLOOP_RUN_TRANSITION_INVALID");x(i),x(a),te(c,s.at(-1)?.eventHash??null)}function x(t){const{snapshotHash:e,...n}=t;if(e!==p("p220-yoloop-progress-snapshot-v1",n))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function te(t,e){const{eventHash:n,...s}=t;if(t.parentEventHash!==e||n!==p("p220-yoloop-run-event-v1",s))throw new Error("YOLOOP_RUN_SNAPSHOT_CONFLICT")}function ne(t,e){return`lease-${oe(`${t}:${e}`)}`}function oe(t){let e=2166136261;for(let n=0;n<t.length;n+=1)e^=t.charCodeAt(n),e=Math.imul(e,16777619)>>>0;return e.toString(36).padStart(7,"0")}function k(t,e){const n=t.leaseExpiresAt??t.ttlExpiresAt,s=Date.parse(n),o=Date.parse(e);return!Number.isFinite(s)||!Number.isFinite(o)?"unknown":s<=o?"expired":"active"}function se(t,e){const n=S(e);return{...t,version:t.version==="1.2"?"1.2":"1.1",lastResetAt:t.lastResetAt??n.lastResetAt,storiesRunToday:t.storiesRunToday??0,iterationsToday:t.iterationsToday??0,iterationsTodayDate:t.iterationsTodayDate??n.iterationsTodayDate,tokensUsedToday:t.tokensUsedToday??0,lastRunAt:t.lastRunAt??null,coolDownUntil:t.coolDownUntil??null,totalRunsAllTime:t.totalRunsAllTime??0,totalStoriesAllTime:t.totalStoriesAllTime??0,currentSession:t.currentSession??null,...t.runControl!==void 0?{runControl:t.runControl}:{},...t.continuationPolicy!==void 0?{continuationPolicy:t.continuationPolicy}:{},...t.continuationSession!==void 0?{continuationSession:t.continuationSession}:{},...t.pendingContinuationAuthorities!==void 0?{pendingContinuationAuthorities:t.pendingContinuationAuthorities}:{},...t.continuationReceipts!==void 0?{continuationReceipts:t.continuationReceipts}:{},...t.continuationRecoveryReceipts!==void 0?{continuationRecoveryReceipts:t.continuationRecoveryReceipts}:{},recovery_history:Array.isArray(t.recovery_history)?H(t.recovery_history):[]}}function H(t){return t.length<=g?t:t.slice(t.length-g)}export{C as YOLOOP_PERSISTENCE_CAPABILITIES,he as YoloopClientStateStore,A as buildYoloopRunTransitionPayloadHash,z as buildYoloopSessionRolloverPayloadHash,W as buildYoloopSuccessorRunPayloadHash,H as capRecoveryHistory,S as createDefaultCounters,ue as createDefaultState,j as fingerprintYoloopSession};
|
|
@@ -415,8 +415,15 @@ function admitArguments(schema, tokens) {
|
|
|
415
415
|
}
|
|
416
416
|
const options = {};
|
|
417
417
|
const positionals = [];
|
|
418
|
-
|
|
419
|
-
|
|
418
|
+
// Free-text diagnosis/research conventionally uses a single dash between
|
|
419
|
+
// the trigger and its prose. Everything after that explicit boundary is
|
|
420
|
+
// opaque user context, so literal strings such as `--targets` are data, not
|
|
421
|
+
// Neocortex options. Without the boundary, unknown options still fail closed.
|
|
422
|
+
const opaqueContextBoundary = schema.inputShape === 'story-or-context'
|
|
423
|
+
&& tokens[0]?.whollyUnquoted === true
|
|
424
|
+
&& /^[-:\u2013\u2014]$/u.test(tokens[0].value);
|
|
425
|
+
let terminated = opaqueContextBoundary;
|
|
426
|
+
for (let index = opaqueContextBoundary ? 1 : 0; index < tokens.length; index += 1) {
|
|
420
427
|
const token = tokens[index];
|
|
421
428
|
if (!terminated && token.whollyUnquoted && token.value === '--') {
|
|
422
429
|
terminated = true;
|
|
@@ -9,107 +9,13 @@
|
|
|
9
9
|
_merge_antigravity_mcp() {
|
|
10
10
|
local stub_file="$1"
|
|
11
11
|
local dest_file="$2"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
echo "[ANTIGRAVITY]
|
|
16
|
-
return
|
|
17
|
-
fi
|
|
18
|
-
|
|
19
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
20
|
-
local tmp="${dest_file}.neocortex.tmp"
|
|
21
|
-
if python3 - "$dest_file" "$stub_file" "$tmp" <<'PYEOF'
|
|
22
|
-
import json, sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
dest_path, stub_path, tmp_path = map(Path, sys.argv[1:4])
|
|
25
|
-
managed = {"playwright", "context7", "browser_use", "figma", "shadcn", "chrome-devtools"}
|
|
26
|
-
try:
|
|
27
|
-
with dest_path.open() as f:
|
|
28
|
-
dest = json.load(f)
|
|
29
|
-
except Exception:
|
|
30
|
-
backup = dest_path.with_name(dest_path.name + ".neocortex-backup-parse-error")
|
|
31
|
-
backup.write_bytes(dest_path.read_bytes())
|
|
32
|
-
print(f"[ANTIGRAVITY] WARN: parseError backupCreated={backup}; original preserved")
|
|
33
|
-
sys.exit(0)
|
|
34
|
-
with stub_path.open() as f:
|
|
35
|
-
stub = json.load(f)
|
|
36
|
-
|
|
37
|
-
if not isinstance(dest, dict):
|
|
38
|
-
backup = dest_path.with_name(dest_path.name + ".neocortex-backup-structural")
|
|
39
|
-
backup.write_bytes(dest_path.read_bytes())
|
|
40
|
-
dest = {}
|
|
41
|
-
print(f"[ANTIGRAVITY] backupCreated={backup}")
|
|
42
|
-
|
|
43
|
-
changed = False
|
|
44
|
-
if not isinstance(dest.get("mcpServers"), dict):
|
|
45
|
-
if "mcpServers" in dest:
|
|
46
|
-
backup = dest_path.with_name(dest_path.name + ".neocortex-backup-structural")
|
|
47
|
-
backup.write_bytes(dest_path.read_bytes())
|
|
48
|
-
print(f"[ANTIGRAVITY] backupCreated={backup}")
|
|
49
|
-
dest["mcpServers"] = {}
|
|
50
|
-
changed = True
|
|
51
|
-
|
|
52
|
-
migrated = []
|
|
53
|
-
for name in list(dest.keys()):
|
|
54
|
-
if name in managed:
|
|
55
|
-
value = dest.pop(name)
|
|
56
|
-
if isinstance(value, dict) and "httpUrl" in value and "serverUrl" not in value:
|
|
57
|
-
value = dict(value)
|
|
58
|
-
value["serverUrl"] = value.pop("httpUrl")
|
|
59
|
-
dest["mcpServers"][name] = value
|
|
60
|
-
migrated.append(name)
|
|
61
|
-
changed = True
|
|
62
|
-
|
|
63
|
-
updated = []
|
|
64
|
-
added = []
|
|
65
|
-
for name, value in stub.get("mcpServers", {}).items():
|
|
66
|
-
if name in dest["mcpServers"]:
|
|
67
|
-
updated.append(name)
|
|
68
|
-
else:
|
|
69
|
-
added.append(name)
|
|
70
|
-
dest["mcpServers"][name] = value
|
|
71
|
-
changed = True
|
|
72
|
-
|
|
73
|
-
if changed:
|
|
74
|
-
with tmp_path.open('w') as f:
|
|
75
|
-
json.dump(dest, f, indent=2)
|
|
76
|
-
f.write('\n')
|
|
77
|
-
else:
|
|
78
|
-
tmp_path.write_text(json.dumps(dest, indent=2) + '\n')
|
|
79
|
-
print("[ANTIGRAVITY] migration summary: "
|
|
80
|
-
f"added={','.join(added) or '-'} updated={','.join(updated) or '-'} "
|
|
81
|
-
f"migrated={','.join(migrated) or '-'} preserved=custom")
|
|
82
|
-
PYEOF
|
|
83
|
-
then
|
|
84
|
-
if [ -f "$tmp" ]; then
|
|
85
|
-
if ! cmp -s "$tmp" "$dest_file"; then
|
|
86
|
-
cp "$dest_file" "${dest_file}.neocortex-backup-structural" 2>/dev/null || true
|
|
87
|
-
mv "$tmp" "$dest_file"
|
|
88
|
-
else
|
|
89
|
-
rm -f "$tmp"
|
|
90
|
-
fi
|
|
91
|
-
fi
|
|
92
|
-
echo "[ANTIGRAVITY] Merged MCP config into: $dest_file (via python3)"
|
|
93
|
-
return 0
|
|
94
|
-
fi
|
|
95
|
-
rm -f "$tmp"
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
if command -v jq >/dev/null 2>&1; then
|
|
99
|
-
local tmp="${dest_file}.neocortex.tmp"
|
|
100
|
-
if jq -s '.[0].mcpServers = ((.[0].mcpServers // {}) * (.[1].mcpServers // {})) | .[0]' "$dest_file" "$stub_file" > "$tmp" 2>/dev/null; then
|
|
101
|
-
cp "$dest_file" "${dest_file}.neocortex-backup-structural" 2>/dev/null || true
|
|
102
|
-
mv "$tmp" "$dest_file"
|
|
103
|
-
echo "[ANTIGRAVITY] Merged MCP config into: $dest_file (via jq)"
|
|
104
|
-
return 0
|
|
105
|
-
fi
|
|
106
|
-
cp "$dest_file" "${dest_file}.neocortex-backup-parse-error" 2>/dev/null || true
|
|
107
|
-
rm -f "$tmp"
|
|
12
|
+
local helper
|
|
13
|
+
helper="$(dirname "$stub_file")/../lib/mcp-merge.js"
|
|
14
|
+
if ! command -v node >/dev/null 2>&1 || [ ! -f "$helper" ]; then
|
|
15
|
+
echo "[ANTIGRAVITY] ERROR: MCP_MERGE_HELPER_UNAVAILABLE; leaving $dest_file untouched" >&2
|
|
16
|
+
return 1
|
|
108
17
|
fi
|
|
109
|
-
|
|
110
|
-
echo "[ANTIGRAVITY] WARN: jq and python3 not found; leaving existing $dest_file untouched"
|
|
111
|
-
echo "[ANTIGRAVITY] WARN: Stub available at: $stub_file"
|
|
112
|
-
return 0
|
|
18
|
+
node "$helper" json "$stub_file" "$dest_file" mcpServers ANTIGRAVITY "playwright,context7,browser_use,figma,shadcn,chrome-devtools"
|
|
113
19
|
}
|
|
114
20
|
|
|
115
21
|
install_antigravity() {
|
|
@@ -166,7 +72,10 @@ install_antigravity() {
|
|
|
166
72
|
|
|
167
73
|
# 3. Additive merge MCP config (preserves user-configured servers)
|
|
168
74
|
if [ -f "$ag_target/mcp-config.json" ]; then
|
|
169
|
-
_merge_antigravity_mcp "$ag_target/mcp-config.json" "$gemini_home/antigravity/mcp_config.json" ||
|
|
75
|
+
_merge_antigravity_mcp "$ag_target/mcp-config.json" "$gemini_home/antigravity/mcp_config.json" || {
|
|
76
|
+
echo "[ANTIGRAVITY] ERROR: MCP merge blocked; user config was preserved." >&2
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
170
79
|
fi
|
|
171
80
|
|
|
172
81
|
# 4. Copy GEMINI.md stub to project root
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: neocortex-root
|
|
3
|
-
description: "🧠 Neocortex Root Agent v4.60.
|
|
3
|
+
description: "🧠 Neocortex Root Agent v4.60.45 | OrNexus Team"
|
|
4
4
|
model: opus
|
|
5
5
|
color: blue
|
|
6
6
|
tools:
|
|
@@ -108,7 +108,7 @@ SEMPRE que este agente for invocado, imprima o banner abaixo como PRIMEIRO outpu
|
|
|
108
108
|
┌────────────────────────────────────────────────────────────┐
|
|
109
109
|
│ │
|
|
110
110
|
│ ####### N E O C O R T E X │
|
|
111
|
-
│ ### ######## v4.60.
|
|
111
|
+
│ ### ######## v4.60.45 │
|
|
112
112
|
│ ######### ##### │
|
|
113
113
|
│ ## ############## Development Orchestrator │
|
|
114
114
|
│ ## ### ###### ## OrNexus Team │
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: neocortex
|
|
3
|
-
description: "🧠 Neocortex v4.60.
|
|
3
|
+
description: "🧠 Neocortex v4.60.45 | OrNexus Team"
|
|
4
4
|
model: opus
|
|
5
5
|
color: blue
|
|
6
6
|
permissionMode: bypassPermissions
|
|
@@ -104,7 +104,7 @@ SEMPRE que este agente for invocado, imprima o banner abaixo como PRIMEIRO outpu
|
|
|
104
104
|
┌────────────────────────────────────────────────────────────┐
|
|
105
105
|
│ │
|
|
106
106
|
│ ####### N E O C O R T E X │
|
|
107
|
-
│ ### ######## v4.60.
|
|
107
|
+
│ ### ######## v4.60.45 │
|
|
108
108
|
│ ######### ##### │
|
|
109
109
|
│ ## ############## Development Orchestrator │
|
|
110
110
|
│ ## ### ###### ## OrNexus Team │
|
|
@@ -28,7 +28,7 @@ Codex built-in commands or actions.
|
|
|
28
28
|
|
|
29
29
|
<!-- END: Plugin Conflict Prevention -->
|
|
30
30
|
|
|
31
|
-
# Neocortex v4.60.
|
|
31
|
+
# Neocortex v4.60.45 | OrNexus Team
|
|
32
32
|
|
|
33
33
|
You are a Development Orchestrator. All orchestration logic is delivered by the remote Neocortex server.
|
|
34
34
|
|