@ornexus/neocortex 4.60.41 → 4.60.43
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 +31 -31
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +14 -2
- package/packages/client/dist/commands/invoke.js +57 -56
- package/packages/client/dist/generated/command-registry.generated.d.ts +66 -23
- package/packages/client/dist/generated/command-registry.generated.js +1 -1
- package/packages/client/dist/generated/public-projections.generated.d.ts +100 -0
- package/packages/client/dist/generated/public-projections.generated.js +1 -0
- package/packages/client/dist/git/git-lifecycle-store.d.ts +17 -0
- package/packages/client/dist/git/git-lifecycle-store.js +4 -4
- package/packages/client/dist/index.d.ts +1 -0
- package/packages/client/dist/index.js +1 -1
- package/packages/client/dist/migrations/project-identity.js +7 -7
- package/packages/client/dist/migrations/state-json.d.ts +14 -0
- package/packages/client/dist/migrations/state-json.js +4 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.d.ts +2 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -1
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -8
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +7 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2 -2
- package/packages/client/dist/state/project-state-snapshot.js +1 -1
- package/packages/client/dist/state/state-json-repair.js +3 -3
- package/packages/client/dist/yoloop/invoke-hooks.d.ts +2 -3
- package/packages/client/dist/yoloop/invoke-hooks.js +9 -8
- package/packages/client/dist/yoloop/shared-discover-epics.d.ts +3 -3
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +87 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +27 -3
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
- package/packages/shared/dist/command-registry-contract.js +38 -0
- package/packages/shared/dist/continuity/goal-operation-authority.js +48 -0
- package/packages/shared/dist/executable-graph-catalog/contracts.js +935 -0
- package/packages/shared/dist/generated/command-registry.generated.js +82 -23
- package/packages/shared/dist/generated/invocation-schema.generated.js +12 -4
- package/packages/shared/dist/generated/public-projections.generated.js +149 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.d.ts +83 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.js +272 -0
- package/packages/shared/dist/invocation/index.d.ts +3 -1
- package/packages/shared/dist/invocation/index.js +40 -1
- package/packages/shared/dist/invocation/recovery-route-compiler.d.ts +56 -0
- package/packages/shared/dist/invocation/recovery-route-compiler.js +214 -0
- package/packages/shared/dist/state/index.d.ts +1 -0
- package/packages/shared/dist/state/index.js +1 -0
- package/packages/shared/dist/state/state-migration.d.ts +58 -0
- package/packages/shared/dist/state/state-migration.js +404 -0
- package/packages/shared/dist/yoloop/canonical.js +15 -0
- package/packages/shared/dist/yoloop/operational-budget.js +164 -0
- package/packages/shared/dist/yoloop/recovery-types.js +39 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- 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 +11 -2
- package/targets-stubs/codex/README.md +7 -4
- package/targets-stubs/codex/neocortex.toml +53 -6
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{existsSync as q,mkdirSync as B,readFileSync as H,renameSync as K,writeFileSync as W,copyFileSync as V}from"node:fs";import{join as E}from"node:path";import{randomBytes as Z}from"node:crypto";import{readGitLifecycleRuntimeSync as U,verifyAcceptedGitLifecycleStoryCheckpointSync as Y}from"../git/git-lifecycle-store.js";class b extends Error{reasonCode;constructor(t,o){super(t),this.reasonCode=o,this.name="StateRepairHardStopError"}}const Q="P189-GAP-STATE-SHAPE",h={reconciled:"epic_aggregate_reconciled",totalRecounted:"epic_total_stories_recounted",completedRecounted:"epic_completed_stories_recounted",statusPromotedDone:"epic_status_promoted_done",completedAtSet:"epic_completed_at_set",stateNotObject:"epic_aggregate_state_not_object",epicsNotObject:"epic_aggregate_epics_not_object",storiesNotObject:"epic_aggregate_stories_not_object",scopeProjected:"epic_aggregate_scope_projected",epicNotObject:"epic_aggregate_epic_not_object",epicIdConflict:"epic_aggregate_epic_id_conflict",storyListNotArray:"epic_aggregate_story_list_not_array",storyRefInvalid:"epic_aggregate_story_ref_invalid",storyRefDuplicate:"epic_aggregate_story_ref_duplicate",storyMissing:"epic_aggregate_story_missing",storyNotObject:"epic_aggregate_story_not_object",storyIdConflict:"epic_aggregate_story_id_conflict",storyEpicConflict:"epic_aggregate_story_epic_conflict",terminalIncompleteConflict:"epic_aggregate_terminal_incomplete_conflict"},X=new Set(["yolo","implement","diagnose","research","write-spec","create-tasks","refactor-suggest","property-tests","validate-tests","qa-gate","update-project-memory","commit-push","sync-main","create-pr","review-pr","merge-pr","review"]),ee=new Set(["yoloop","auto-yolo","epic-runner","story-runner","loop-yolo","auto-epic"]);function l(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function x(){return new Date().toISOString()}function te(){return x().replace(/[:.]/g,"-")}function k(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"&&t.length>0):[]}function $(e){const[t]=e.trim().split(/\s+/,1);return t?t.replace(/^@?\*/,"").replace(/^@/,"").toLowerCase():null}function oe(e){const[t]=e.trim().split(/\s+/,1);if(!t)return!1;const o=t.replace(/^@/,"");return/^[A-Z]?\d+(?:\.\d+)+$/i.test(o)||/(?:^|\/)[A-Z]?\d+(?:\.\d+)+\.story\.md$/i.test(o)}function O(e){const t=e.match(/(?:^|[/@\s])([A-Z]?\d+(?:\.\d+)+)\.story\.md\b/i);return t?.[1]?t[1]:e.match(/(?:^|\s|@)([A-Z]?\d+(?:\.\d+)+)(?=$|\s)/i)?.[1]??null}function P(e,t){if(typeof e!="string")return!1;const o=t??$(e);return(o==="yolo"||o==="yolo-inline")&&T(e).length>0&&O(e)===null}function T(e){const t=e.trim(),o=t.search(/\s/);return o===-1?"":t.slice(o).trim()}function re(e){const t=T(e).split(/\s+/).find(o=>o.length>0&&!o.startsWith("-"));return t?t.replace(/^@/,"").replace(/^['"]|['"]$/g,""):null}function D(e,t){const r=e.replace(/^@/,"").replace(/^epic-/i,"").replace(/\.md$/i,""),s=[r,r.toUpperCase(),/^p\d+$/i.test(r)?`P${r.replace(/^p/i,"")}`:null,/^\d+$/.test(r)?`P${r}`:null].filter(a=>typeof a=="string"&&a.length>0);for(const a of s)if(l(t[a]))return a;return null}function se(e,t){const o=e.match(/(?:^|[/@\s])epic-(P?\d+)\.md\b/i);return o?.[1]?D(o[1],t):null}function L(e,t,o){if(typeof t!="string"||t.trim().length===0||!l(e.epics)||!l(e.stories))return;const r=t.trim(),s=o??$(r);if(s!==null&&X.has(s)||oe(r)){const u=O(r),p=u?e.stories[u]:void 0;if(l(p)&&typeof p.epic_id=="string"&&l(e.epics[p.epic_id]))return p.epic_id;const n=u?.split(".")[0];if(n&&l(e.epics[n]))return n}if(s!==null&&ee.has(s)){const u=se(r,e.epics);if(u)return u;const p=re(r);if(p){const n=D(p,e.epics);if(n)return n}}}const R="pr_merged_issue_closed_reconciled",ne="invalid_workflow_issue_removed";function w(e){return typeof e=="string"?e.trim().toLowerCase().replace(/[_\s]+/g,"-"):""}const ie=new Set(["done","completed","merged"]),ce=new Set(["done","completed","merged"]);function M(e){return ie.has(w(e))}function G(e){return ce.has(w(e))}function S(e,t){throw new b(`${t} (${Q} if this shape is intentionally new).`,e)}function ae(e){return JSON.parse(JSON.stringify(e))}function pe(e,t){const o=t.scope??e.scope;if(!l(o))return;const r=typeof o.kind=="string"?o.kind:null,s=typeof o.omittedStoryCount=="number"?o.omittedStoryCount:0,a=typeof o.omittedEpicCount=="number"?o.omittedEpicCount:0;(r==="projected"||s>0||a>0)&&S(h.scopeProjected,"[Neocortex] state.json aggregate reconciliation stopped: projected or partial state coverage cannot prove epic aggregates.")}function le(e,t){const o=[],r=new Set;for(const s of t)(typeof s!="string"||s.length===0)&&S(h.storyRefInvalid,`[Neocortex] state.json aggregate reconciliation stopped: epic "${y(e)}" has a non-string or empty story reference.`),r.has(s)&&S(h.storyRefDuplicate,`[Neocortex] state.json aggregate reconciliation stopped: epic "${y(e)}" lists story "${y(s)}" more than once.`),r.add(s),o.push(s);return o}function de(e,t,o){const r=o[t];return r===void 0&&S(h.storyMissing,`[Neocortex] state.json aggregate reconciliation stopped: epic "${y(e)}" references missing story "${y(t)}".`),l(r)||S(h.storyNotObject,`[Neocortex] state.json aggregate reconciliation stopped: story "${y(t)}" is not an object.`),r.id!==t&&S(h.storyIdConflict,`[Neocortex] state.json aggregate reconciliation stopped: story key "${y(t)}" conflicts with its id field.`),r.epic_id!==e&&S(h.storyEpicConflict,`[Neocortex] state.json aggregate reconciliation stopped: story "${y(t)}" does not belong to epic "${y(e)}".`),r}function C(e,t){e.includes(t)||e.push(t)}function ue(e,t={}){l(e)||S(h.stateNotObject,"[Neocortex] state.json aggregate reconciliation stopped: top-level state must be an object."),l(e.epics)||S(h.epicsNotObject,"[Neocortex] state.json aggregate reconciliation stopped: `epics` must be an object before counts are authoritative."),l(e.stories)||S(h.storiesNotObject,"[Neocortex] state.json aggregate reconciliation stopped: `stories` must be an object before counts are authoritative."),pe(e,t);const o=ae(e),r=o.epics,s=o.stories,a=[],u=[],p=new Set(t.effectiveDoneStoryIds??[]);for(const[n,c]of Object.entries(r)){if(t.targetEpicId&&n!==t.targetEpicId)continue;l(c)||S(h.epicNotObject,`[Neocortex] state.json aggregate reconciliation stopped: epic "${y(n)}" is not an object.`),c.id!==n&&S(h.epicIdConflict,`[Neocortex] state.json aggregate reconciliation stopped: epic key "${y(n)}" conflicts with its id field.`),Array.isArray(c.stories)||S(h.storyListNotArray,`[Neocortex] state.json aggregate reconciliation stopped: epic "${y(n)}" has non-array stories.`);const g=le(n,c.stories),i=g.length,d=g.filter(j=>{const _=de(n,j,s);return M(_.status)||p.has(j)}).length,f=i>0&&d===i;if(G(c.status)&&!f&&i>0&&S(h.terminalIncompleteConflict,`[Neocortex] state.json aggregate reconciliation stopped: terminal epic "${y(n)}" has incomplete listed stories.`),t.preserveGitLifecycleAggregates&&(c.branch_mode==="epic"||typeof c.execution_branch=="string"))continue;const m=[],v={total_stories:c.total_stories,completed_stories:c.completed_stories,status:c.status,completed_at:c.completed_at};if(c.total_stories!==i&&(c.total_stories=i,C(m,h.totalRecounted)),c.completed_stories!==d&&(c.completed_stories=d,C(m,h.completedRecounted)),!G(c.status)&&f&&(c.status="done",C(m,h.statusPromotedDone),(c.completed_at===void 0||c.completed_at===null)&&(c.completed_at=t.nowIso??x(),C(m,h.completedAtSet))),m.length>0){C(m,h.reconciled);for(const j of m)C(a,j);u.push({epicId:n,reasonCodes:m,previous:v,next:{total_stories:i,completed_stories:d,status:c.status,completed_at:c.completed_at}})}}return{mutated:JSON.stringify(o)!==JSON.stringify(e),state:o,reasonCodes:a,changes:u}}function A(e,t){const o=e[t];return l(o)?o:null}function _e(e){const t=A(e,"issueCloseAudit")??A(e,"issue_close_audit");if(t){const s=w(t.event).replace(/-/g,"_"),a=t.publicSafe===!0||t.public_safe===!0,u=t.rawLogStorage==="forbidden"||t.raw_log_storage==="forbidden";if(s==="story_issue_closed"&&a&&u)return!0}const o=A(e,"completionEvidence")??A(e,"completion_evidence"),r=w(o?.issueState??o?.issue_state??o?.state);return e.issue_closed===!0||w(e.issue_state??e.issueState??e.workflow_issue_state)==="closed"||r==="closed"}function z(e){const t=A(e,"completionEvidence")??A(e,"completion_evidence"),o=A(e,"scopedCiEvidence")??A(e,"scoped_ci_evidence"),r=[e.physicalMergeStatus,e.pr_state,e.prState,e.pull_request_state,e.pullRequestState,t?.prState,t?.pr_state,t?.pullRequestState,o?.prState].map(w);return e.merged===!0||r.includes("merged")||w(o?.finalState)==="can-merge"&&e.merged===!0}function fe(e){const t=A(e,"completionEvidence")??A(e,"completion_evidence"),o=[e.pr_state,e.prState,e.pull_request_state,e.pullRequestState,t?.prState,t?.pr_state,t?.pullRequestState].map(w),r=z(e),s=["denied-external-gate","denied-local-or-repo","not-attempted"].includes(w(e.physicalMergeStatus)),a=o.includes("closed")||o.includes("closed-unmerged");return r&&(e.merged===!1||s||a)||e.merged===!0&&s}function ge(e,t,o){const r=`state-completion-reconciliation:${y(t)}`,s=e.filter(l).map(n=>({...n})),a={id:r,status:"open",reason:"stale_state_completion_evidence_observed",reason_codes:[R],category:"state/completion-reconciliation",story_id:y(t),public_safe:!0,raw_log_storage:"forbidden",created_at:o,last_seen_at:o,occurrences:1},u=s.findIndex(n=>n.id===r);if(u<0)return[...s,a];const p=s[u];return s[u]={...p,...a,created_at:typeof p.created_at=="string"?p.created_at:o,occurrences:Math.max(1,typeof p.occurrences=="number"?p.occurrences:1)+1},s}function me(e,t,o,r){const s=l(e.stories)?e.stories:null;if(!s)return;const a=[],u=x();for(const[p,n]of Object.entries(s)){if(!l(n))continue;const c=typeof n.epic_id=="string"?n.epic_id:p.split(".")[0];if(r&&c!==r)continue;const g=w(n.status);if(fe(n))throw new b(`[Neocortex] state.json repair stopped: story "${p}" has conflicting PR merge completion evidence. Resolve .neocortex/state.json manually before invoking Neocortex.`,"conflicting_completion_evidence");if(g!=="pr-reviewed"||!z(n)||!_e(n))continue;const i=typeof n.status=="string"?n.status:g,d=k(n.steps_completed);n.status="done",n.last_step="step-c-12-merge-pr",n.steps_completed=Array.from(new Set([...d,"step-c-12-merge-pr"])),n.merged=!0,n.physicalMergeStatus="merged",n.completion_reconciled_at=u,t.push(`reconciled completion evidence for story ${p}`),o.push(R),a.push({schemaVersion:1,publicSafe:!0,rawLogStorage:"forbidden",storyId:y(p),previousStatus:i,nextStatus:"done",reasonCodes:[R],reconciledAt:u}),e.state_repair_debt=ge(Array.isArray(e.state_repair_debt)?e.state_repair_debt:[],p,u)}a.length>0&&(e.state_completion_reconciliations=[...Array.isArray(e.state_completion_reconciliations)?e.state_completion_reconciliations.filter(l):[],...a])}function ye(e,t,o){const r=l(e.stories)?e.stories:null;if(r)for(const[s,a]of Object.entries(r))!l(a)||!Object.prototype.hasOwnProperty.call(a,"workflow_issue")||typeof a.workflow_issue=="number"&&Number.isFinite(a.workflow_issue)||(delete a.workflow_issue,t.push(`removed invalid workflow_issue from story ${y(s)}`),o.push(ne))}function he(e,t,o){const r=o[e];if(!l(r))return;const s=r.epic_id;if(typeof s=="string"&&s!==t)throw new b(`[Neocortex] state.json repair stopped: story "${e}" belongs to epic "${s}" but legacy planning maps it to "${t}". Resolve the conflicting IDs in .neocortex/state.json, then rerun the command.`,"conflicting_story_epic_id")}function Se(e,t={}){if(Array.isArray(e.stories))throw new b("[Neocortex] state.json repair stopped: `stories` is an Array. Convert it to an object keyed by story ID or run `*migrate-state --preview` for guidance.","stories_array_ambiguous");if(l(e.epics)){for(const[i,d]of Object.entries(e.epics))if(l(d)&&d.stories!==void 0&&!Array.isArray(d.stories))throw new b(`[Neocortex] state.json repair stopped: epic "${i}" has non-array \`stories\`. Repair this ambiguous shape manually before invoking Neocortex.`,"epic_stories_not_array")}if(e.schema_version!==void 0&&e.schema_version!==1)throw new b(`[Neocortex] state.json repair stopped: schema_version=${String(e.schema_version)} is not a safe automatic downgrade target. Run \`*migrate-state --preview\` or repair the schema version manually.`,"unsupported_schema_version");if(P(t.args,t.canonicalCommandId)){for(const i of["epics","stories"]){const d=e[i];if(d!==void 0){if(!l(d))throw new b("[Neocortex] inline materialization stopped: invalid state collection.","inline_state_collection_invalid");for(const[f,m]of Object.entries(d)){if(!l(m))throw new b("[Neocortex] inline materialization stopped: invalid state record.","inline_state_record_invalid");if(m.id!==void 0&&m.id!==f)throw new b("[Neocortex] inline materialization stopped: conflicting state identity.","inline_state_identity_conflict")}}}return{mutated:!1,state:e,reasonCodes:[],changes:[]}}let o=JSON.parse(JSON.stringify(e));const r=[],s=[];let a=[];l(o.epics)||(o.epics={},r.push("initialized epics{}"),s.push("initialized_epics_object")),l(o.stories)||(o.stories={},r.push("initialized stories{}"),s.push("initialized_stories_object"));const u=o.epics,p=o.stories,n=L(o,t.args,t.canonicalCommandId),c=l(o.planning)?o.planning:null;if(c&&l(c.current)){const i=c.current,d=typeof i.epic_id=="string"?i.epic_id:null;if(!d)throw new b("[Neocortex] state.json repair stopped: legacy planning.current has no epic_id. Add the missing epic_id or run `*migrate-state --preview`.","planning_current_missing_epic_id");const f=Array.isArray(i.stories)?i.stories:[],m=f.map(_=>l(_)&&typeof _.id=="string"?_.id:typeof _=="string"?_:null).filter(_=>!!_);for(const _ of m)he(_,d,p);l(u[d])||(u[d]={id:d,title:typeof i.title=="string"?i.title:`Epic ${d}`,status:i.status==="done"?"done":"in-progress",stories:m,created_at:typeof i.created_at=="string"?i.created_at:x(),completed_at:typeof i.completed_at=="string"?i.completed_at:null,completed_stories:typeof i.completed_stories=="number"?i.completed_stories:0,total_stories:typeof i.total_stories=="number"?i.total_stories:m.length,source:"client-auto-repair-P156.05"},r.push(`migrated planning.current to epics["${d}"]`),s.push("legacy_planning_current_migrated"));for(const _ of f)!l(_)||typeof _.id!="string"||l(p[_.id])||(p[_.id]={..._,id:_.id,epic_id:d,title:typeof _.title=="string"?_.title:_.id,status:typeof _.status=="string"?_.status:"backlog",depends_on:k(_.depends_on),created_at:typeof _.created_at=="string"?_.created_at:x()},r.push(`migrated embedded story ${_.id}`),s.push("legacy_planning_story_migrated"));const v=typeof i.status=="string"?i.status.trim().toLowerCase().replace(/[_\s]+/g,"-"):"",I=new Set(["epic_id","title","status","created_at","updated_at","completed_at","completed_stories","total_stories","stories"]),j=Object.keys(i).filter(_=>!I.has(_));v==="generated"&&j.length===0?(delete c.current,r.push("removed safe legacy planning.current generated container"),s.push("legacy_planning_current_generated_removed")):j.length>0&&(r.push(`preserved planning.current unknown keys: ${j.sort().join(", ")}`),s.push("unknown_planning_current_keys_preserved"))}if(c&&Array.isArray(c.history)){let i=!0;const d=new Set(["epic_id","title","status","created_at","completed_at","completed_stories","total_stories","stories"]);for(const f of c.history){if(!l(f)){i=!1;continue}Object.keys(f).filter(I=>!d.has(I)).length>0&&(i=!1);const v=typeof f.epic_id=="string"?f.epic_id:null;if(!v){i=!1;continue}l(u[v])||(u[v]={id:v,title:typeof f.title=="string"?f.title:`Epic ${v}`,status:"done",stories:k(f.stories),created_at:typeof f.created_at=="string"?f.created_at:x(),completed_at:typeof f.completed_at=="string"?f.completed_at:x(),completed_stories:typeof f.completed_stories=="number"?f.completed_stories:0,total_stories:typeof f.total_stories=="number"?f.total_stories:0,source:"client-auto-repair-P156.05"},r.push(`migrated planning.history to epics["${v}"]`),s.push("legacy_planning_history_migrated"))}i?(delete c.history,r.push("removed safe legacy planning.history container"),s.push("legacy_planning_history_removed")):(r.push("preserved planning.history because one or more entries were not safely removable"),s.push("legacy_planning_history_preserved"))}if(c&&Object.keys(c).length===0&&(delete o.planning,r.push("removed empty planning container"),s.push("empty_planning_removed")),o.schema_version!==1&&(o.schema_version=1,r.push("set schema_version=1"),s.push("schema_version_added")),ye(o,r,s),me(o,r,s,n),typeof t.args=="string"){const i=ue(o,{nowIso:t.nowIso,targetEpicId:n,effectiveDoneStoryIds:t.effectiveDoneStoryIds,preserveGitLifecycleAggregates:!0});if(i.mutated){o=i.state,a=i.changes;for(const d of i.reasonCodes)s.push(d);for(const d of i.changes)r.push(`reconciled aggregate for epic ${y(d.epicId)}`)}}const g=JSON.stringify(o)!==JSON.stringify(e);if(g){const i=t.nowIso??x(),d=Array.from(new Set(s));a.length>0&&we(o,a,i),o.last_updated=i,o.state_migration_repairs=[...Array.isArray(o.state_migration_repairs)?o.state_migration_repairs:[],{schemaVersion:1,action:"client_auto_repair",reasonCodes:d,repairedAt:i}],o.state_repair_debt=Ae(Array.isArray(o.state_repair_debt)?o.state_repair_debt:[],d,i)}return{mutated:g,state:o,reasonCodes:Array.from(new Set(s)),changes:r}}function F(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function J(e){const t=w(e);return t.length>0?N(t).slice(0,80):null}function be(e,t){return{schemaVersion:1,publicSafe:!0,epicId:y(e.epicId),reasonCodes:e.reasonCodes.map(N).filter(Boolean),previous:{total_stories:F(e.previous.total_stories),completed_stories:F(e.previous.completed_stories),status:J(e.previous.status),completed_at_present:typeof e.previous.completed_at=="string"&&e.previous.completed_at.length>0},next:{total_stories:e.next.total_stories,completed_stories:e.next.completed_stories,status:J(e.next.status),completed_at_present:typeof e.next.completed_at=="string"&&e.next.completed_at.length>0},repairedAt:t}}function ve(e,t,o){let r=e.filter(l).map(s=>({...s}));for(const s of t){const a=y(s.epicId),u=Array.from(new Set(s.reasonCodes.map(N).filter(Boolean))).sort(),p=`state-aggregate-reconciliation:${a}`,n={id:p,status:"open",reason:"epic_aggregate_reconciled",reason_codes:u,category:"state/epic-aggregate-reconciliation",epic_id:a,created_at:o,last_seen_at:o,occurrences:1},c=r.findIndex(i=>i.id===p);if(c<0){r=[...r,n];continue}const g=r[c];r[c]={...g,...n,created_at:typeof g.created_at=="string"?g.created_at:o,occurrences:Math.max(1,typeof g.occurrences=="number"?g.occurrences:1)+1}}return r}function we(e,t,o){e.state_aggregate_reconciliations=[...Array.isArray(e.state_aggregate_reconciliations)?e.state_aggregate_reconciliations.filter(l):[],...t.map(r=>be(r,o))],e.state_repair_debt=ve(Array.isArray(e.state_repair_debt)?e.state_repair_debt:[],t,o)}function Ae(e,t,o){const r=Array.from(new Set(t.map(N).filter(Boolean))).sort(),s=`state-drift-repair:${r.join("+")||"unknown"}`,a=e.filter(l).map(c=>({...c})),u={id:s,status:"open",reason:"state_json_drift_repaired",reason_codes:r,category:"state/drift-repair",created_at:o,last_seen_at:o,occurrences:1},p=a.findIndex(c=>c.id===s);if(p<0)return[...a,u];const n=a[p];return a[p]={...n,...u,created_at:typeof n.created_at=="string"?n.created_at:o,occurrences:Math.max(1,typeof n.occurrences=="number"?n.occurrences:1)+1},a}function N(e){return e.replace(/\b(sk|ghp|gho|github_pat|xoxb|AKIA)[A-Za-z0-9_-]{8,}\b/g,"[redacted]").replace(/(password|passwd|secret|token|api[_-]?key)\s*[:=]\s*[^,\s)]+/gi,"$1=[redacted]").replace(/https?:\/\/[^\s)]+/gi,"[url-redacted]").replace(/[^a-z0-9_+-]+/gi,"_").replace(/^_+|_+$/g,"").slice(0,120)}function y(e){return e.replace(/[^a-z0-9_.:-]+/gi,"_").replace(/^_+|_+$/g,"").slice(0,120)||"unknown"}function Ne(e,t={}){const o=E(e,".neocortex"),r=E(o,"state.json");if(!q(r))return{repaired:!1,reasonCodes:[],changes:[]};let s;try{s=JSON.parse(H(r,"utf8"))}catch(d){const f=d instanceof Error?d.message:String(d);throw new b(`[Neocortex] state.json repair stopped: invalid JSON in .neocortex/state.json (${f}). No files were overwritten. Fix the JSON or restore a backup, then rerun.`,"invalid_json")}if(!l(s))throw new b("[Neocortex] state.json repair stopped: top-level state must be a JSON object. Restore a valid .neocortex/state.json before invoking Neocortex.","state_not_object");const a=L(s,t.args,t.canonicalCommandId),u=P(t.args,t.canonicalCommandId)?[]:je(e,s,a),p=Se(s,{...t,effectiveDoneStoryIds:u});if(!p.mutated)return{repaired:!1,reasonCodes:[],changes:[]};B(o,{recursive:!0});const n=te(),c=E(o,`state.json.backup-${n}`),g=E(o,`state.json.tmp.${Z(6).toString("hex")}`);V(r,c),W(g,`${JSON.stringify(p.state,null,2)}
|
|
2
|
-
`,"utf8"),
|
|
3
|
-
`),{repaired:!0,reasonCodes:
|
|
1
|
+
import{existsSync as F,mkdirSync as J,readFileSync as q,renameSync as B,writeFileSync as H,copyFileSync as W}from"node:fs";import{join as x}from"node:path";import{randomBytes as V}from"node:crypto";import{planStateMigration as Z,StateMigrationHardStopError as U}from"../../../shared/dist/state/index.js";import{applyStateMigration as K,previewStateMigration as Y,StateMigrationApplyError as Q}from"../migrations/state-json.js";import{readGitLifecycleRuntimeSync as X,verifyAcceptedGitLifecycleStoryCheckpointSync as ee}from"../git/git-lifecycle-store.js";class S extends Error{reasonCode;constructor(t,o){super(t),this.reasonCode=o,this.name="StateRepairHardStopError"}}const te="P189-GAP-STATE-SHAPE",f={reconciled:"epic_aggregate_reconciled",totalRecounted:"epic_total_stories_recounted",completedRecounted:"epic_completed_stories_recounted",statusPromotedDone:"epic_status_promoted_done",completedAtSet:"epic_completed_at_set",stateNotObject:"epic_aggregate_state_not_object",epicsNotObject:"epic_aggregate_epics_not_object",storiesNotObject:"epic_aggregate_stories_not_object",scopeProjected:"epic_aggregate_scope_projected",epicNotObject:"epic_aggregate_epic_not_object",epicIdConflict:"epic_aggregate_epic_id_conflict",storyListNotArray:"epic_aggregate_story_list_not_array",storyRefInvalid:"epic_aggregate_story_ref_invalid",storyRefDuplicate:"epic_aggregate_story_ref_duplicate",storyMissing:"epic_aggregate_story_missing",storyNotObject:"epic_aggregate_story_not_object",storyIdConflict:"epic_aggregate_story_id_conflict",storyEpicConflict:"epic_aggregate_story_epic_conflict",terminalIncompleteConflict:"epic_aggregate_terminal_incomplete_conflict"},oe=new Set(["yolo","implement","diagnose","research","write-spec","create-tasks","refactor-suggest","property-tests","validate-tests","qa-gate","update-project-memory","commit-push","sync-main","create-pr","review-pr","merge-pr","review"]),re=new Set(["yoloop","auto-yolo","epic-runner","story-runner","loop-yolo","auto-epic"]);function d(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function j(){return new Date().toISOString()}function ne(){return j().replace(/[:.]/g,"-")}function ie(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"&&t.length>0):[]}function k(e){const[t]=e.trim().split(/\s+/,1);return t?t.replace(/^@?\*/,"").replace(/^@/,"").toLowerCase():null}function se(e){const[t]=e.trim().split(/\s+/,1);if(!t)return!1;const o=t.replace(/^@/,"");return/^[A-Z]?\d+(?:\.\d+)+$/i.test(o)||/(?:^|\/)[A-Z]?\d+(?:\.\d+)+\.story\.md$/i.test(o)}function R(e){const t=e.match(/(?:^|[/@\s])([A-Z]?\d+(?:\.\d+)+)\.story\.md\b/i);return t?.[1]?t[1]:e.match(/(?:^|\s|@)([A-Z]?\d+(?:\.\d+)+)(?=$|\s)/i)?.[1]??null}function I(e,t){if(typeof e!="string")return!1;const o=t??k(e);return(o==="yolo"||o==="yolo-inline")&&$(e).length>0&&R(e)===null}function $(e){const t=e.trim(),o=t.search(/\s/);return o===-1?"":t.slice(o).trim()}function ce(e){const t=$(e).split(/\s+/).find(o=>o.length>0&&!o.startsWith("-"));return t?t.replace(/^@/,"").replace(/^['"]|['"]$/g,""):null}function O(e,t){const r=e.replace(/^@/,"").replace(/^epic-/i,"").replace(/\.md$/i,""),s=[r,r.toUpperCase(),/^p\d+$/i.test(r)?`P${r.replace(/^p/i,"")}`:null,/^\d+$/.test(r)?`P${r}`:null].filter(c=>typeof c=="string"&&c.length>0);for(const c of s)if(d(t[c]))return c;return null}function ae(e,t){const o=e.match(/(?:^|[/@\s])epic-(P?\d+)\.md\b/i);return o?.[1]?O(o[1],t):null}function P(e,t,o){if(typeof t!="string"||t.trim().length===0||!d(e.epics)||!d(e.stories))return;const r=t.trim(),s=o??k(r);if(s!==null&&oe.has(s)||se(r)){const l=R(r),a=l?e.stories[l]:void 0;if(d(a)&&typeof a.epic_id=="string"&&d(e.epics[a.epic_id]))return a.epic_id;const n=l?.split(".")[0];if(n&&d(e.epics[n]))return n}if(s!==null&&re.has(s)){const l=ae(r,e.epics);if(l)return l;const a=ce(r);if(a){const n=O(a,e.epics);if(n)return n}}}const E="pr_merged_issue_closed_reconciled",pe="invalid_workflow_issue_removed";function b(e){return typeof e=="string"?e.trim().toLowerCase().replace(/[_\s]+/g,"-"):""}const le=new Set(["done","completed","merged"]),de=new Set(["done","completed","merged"]);function T(e){return le.has(b(e))}function M(e){return de.has(b(e))}function h(e,t){throw new S(`${t} (${te} if this shape is intentionally new).`,e)}function ue(e){return JSON.parse(JSON.stringify(e))}function fe(e,t){const o=t.scope??e.scope;if(!d(o))return;const r=typeof o.kind=="string"?o.kind:null,s=typeof o.omittedStoryCount=="number"?o.omittedStoryCount:0,c=typeof o.omittedEpicCount=="number"?o.omittedEpicCount:0;(r==="projected"||s>0||c>0)&&h(f.scopeProjected,"[Neocortex] state.json aggregate reconciliation stopped: projected or partial state coverage cannot prove epic aggregates.")}function ge(e,t){const o=[],r=new Set;for(const s of t)(typeof s!="string"||s.length===0)&&h(f.storyRefInvalid,`[Neocortex] state.json aggregate reconciliation stopped: epic "${u(e)}" has a non-string or empty story reference.`),r.has(s)&&h(f.storyRefDuplicate,`[Neocortex] state.json aggregate reconciliation stopped: epic "${u(e)}" lists story "${u(s)}" more than once.`),r.add(s),o.push(s);return o}function _e(e,t,o){const r=o[t];return r===void 0&&h(f.storyMissing,`[Neocortex] state.json aggregate reconciliation stopped: epic "${u(e)}" references missing story "${u(t)}".`),d(r)||h(f.storyNotObject,`[Neocortex] state.json aggregate reconciliation stopped: story "${u(t)}" is not an object.`),r.id!==t&&h(f.storyIdConflict,`[Neocortex] state.json aggregate reconciliation stopped: story key "${u(t)}" conflicts with its id field.`),r.epic_id!==e&&h(f.storyEpicConflict,`[Neocortex] state.json aggregate reconciliation stopped: story "${u(t)}" does not belong to epic "${u(e)}".`),r}function v(e,t){e.includes(t)||e.push(t)}function me(e,t={}){d(e)||h(f.stateNotObject,"[Neocortex] state.json aggregate reconciliation stopped: top-level state must be an object."),d(e.epics)||h(f.epicsNotObject,"[Neocortex] state.json aggregate reconciliation stopped: `epics` must be an object before counts are authoritative."),d(e.stories)||h(f.storiesNotObject,"[Neocortex] state.json aggregate reconciliation stopped: `stories` must be an object before counts are authoritative."),fe(e,t);const o=ue(e),r=o.epics,s=o.stories,c=[],l=[],a=new Set(t.effectiveDoneStoryIds??[]);for(const[n,i]of Object.entries(r)){if(t.targetEpicId&&n!==t.targetEpicId)continue;d(i)||h(f.epicNotObject,`[Neocortex] state.json aggregate reconciliation stopped: epic "${u(n)}" is not an object.`),i.id!==n&&h(f.epicIdConflict,`[Neocortex] state.json aggregate reconciliation stopped: epic key "${u(n)}" conflicts with its id field.`),Array.isArray(i.stories)||h(f.storyListNotArray,`[Neocortex] state.json aggregate reconciliation stopped: epic "${u(n)}" has non-array stories.`);const p=ge(n,i.stories),g=p.length,m=p.filter(A=>{const z=_e(n,A,s);return T(z.status)||a.has(A)}).length,_=g>0&&m===g;if(M(i.status)&&!_&&g>0&&h(f.terminalIncompleteConflict,`[Neocortex] state.json aggregate reconciliation stopped: terminal epic "${u(n)}" has incomplete listed stories.`),t.preserveGitLifecycleAggregates&&(i.branch_mode==="epic"||typeof i.execution_branch=="string"))continue;const y=[],N={total_stories:i.total_stories,completed_stories:i.completed_stories,status:i.status,completed_at:i.completed_at};if(i.total_stories!==g&&(i.total_stories=g,v(y,f.totalRecounted)),i.completed_stories!==m&&(i.completed_stories=m,v(y,f.completedRecounted)),!M(i.status)&&_&&(i.status="done",v(y,f.statusPromotedDone),(i.completed_at===void 0||i.completed_at===null)&&(i.completed_at=t.nowIso??j(),v(y,f.completedAtSet))),y.length>0){v(y,f.reconciled);for(const A of y)v(c,A);l.push({epicId:n,reasonCodes:y,previous:N,next:{total_stories:g,completed_stories:m,status:i.status,completed_at:i.completed_at}})}}return{mutated:JSON.stringify(o)!==JSON.stringify(e),state:o,reasonCodes:c,changes:l}}function w(e,t){const o=e[t];return d(o)?o:null}function ye(e){const t=w(e,"issueCloseAudit")??w(e,"issue_close_audit");if(t){const s=b(t.event).replace(/-/g,"_"),c=t.publicSafe===!0||t.public_safe===!0,l=t.rawLogStorage==="forbidden"||t.raw_log_storage==="forbidden";if(s==="story_issue_closed"&&c&&l)return!0}const o=w(e,"completionEvidence")??w(e,"completion_evidence"),r=b(o?.issueState??o?.issue_state??o?.state);return e.issue_closed===!0||b(e.issue_state??e.issueState??e.workflow_issue_state)==="closed"||r==="closed"}function D(e){const t=w(e,"completionEvidence")??w(e,"completion_evidence"),o=w(e,"scopedCiEvidence")??w(e,"scoped_ci_evidence"),r=[e.physicalMergeStatus,e.pr_state,e.prState,e.pull_request_state,e.pullRequestState,t?.prState,t?.pr_state,t?.pullRequestState,o?.prState].map(b);return e.merged===!0||r.includes("merged")||b(o?.finalState)==="can-merge"&&e.merged===!0}function he(e){const t=w(e,"completionEvidence")??w(e,"completion_evidence"),o=[e.pr_state,e.prState,e.pull_request_state,e.pullRequestState,t?.prState,t?.pr_state,t?.pullRequestState].map(b),r=D(e),s=["denied-external-gate","denied-local-or-repo","not-attempted"].includes(b(e.physicalMergeStatus)),c=o.includes("closed")||o.includes("closed-unmerged");return r&&(e.merged===!1||s||c)||e.merged===!0&&s}function Se(e,t,o){const r=`state-completion-reconciliation:${u(t)}`,s=e.filter(d).map(n=>({...n})),c={id:r,status:"open",reason:"stale_state_completion_evidence_observed",reason_codes:[E],category:"state/completion-reconciliation",story_id:u(t),public_safe:!0,raw_log_storage:"forbidden",created_at:o,last_seen_at:o,occurrences:1},l=s.findIndex(n=>n.id===r);if(l<0)return[...s,c];const a=s[l];return s[l]={...a,...c,created_at:typeof a.created_at=="string"?a.created_at:o,occurrences:Math.max(1,typeof a.occurrences=="number"?a.occurrences:1)+1},s}function be(e,t,o,r){const s=d(e.stories)?e.stories:null;if(!s)return;const c=[],l=j();for(const[a,n]of Object.entries(s)){if(!d(n))continue;const i=typeof n.epic_id=="string"?n.epic_id:a.split(".")[0];if(r&&i!==r)continue;const p=b(n.status);if(he(n))throw new S(`[Neocortex] state.json repair stopped: story "${a}" has conflicting PR merge completion evidence. Resolve .neocortex/state.json manually before invoking Neocortex.`,"conflicting_completion_evidence");if(p!=="pr-reviewed"||!D(n)||!ye(n))continue;const g=typeof n.status=="string"?n.status:p,m=ie(n.steps_completed);n.status="done",n.last_step="step-c-12-merge-pr",n.steps_completed=Array.from(new Set([...m,"step-c-12-merge-pr"])),n.merged=!0,n.physicalMergeStatus="merged",n.completion_reconciled_at=l,t.push(`reconciled completion evidence for story ${a}`),o.push(E),c.push({schemaVersion:1,publicSafe:!0,rawLogStorage:"forbidden",storyId:u(a),previousStatus:g,nextStatus:"done",reasonCodes:[E],reconciledAt:l}),e.state_repair_debt=Se(Array.isArray(e.state_repair_debt)?e.state_repair_debt:[],a,l)}c.length>0&&(e.state_completion_reconciliations=[...Array.isArray(e.state_completion_reconciliations)?e.state_completion_reconciliations.filter(d):[],...c])}function we(e,t,o){const r=d(e.stories)?e.stories:null;if(r)for(const[s,c]of Object.entries(r))!d(c)||!Object.prototype.hasOwnProperty.call(c,"workflow_issue")||typeof c.workflow_issue=="number"&&Number.isFinite(c.workflow_issue)||(delete c.workflow_issue,t.push(`removed invalid workflow_issue from story ${u(s)}`),o.push(pe))}function ve(e,t={}){if(Array.isArray(e.stories))throw new S("[Neocortex] state.json repair stopped: `stories` is an Array. Convert it to an object keyed by story ID or run `*migrate-state --preview` for guidance.","stories_array_ambiguous");if(d(e.epics)){for(const[i,p]of Object.entries(e.epics))if(d(p)&&p.stories!==void 0&&!Array.isArray(p.stories))throw new S(`[Neocortex] state.json repair stopped: epic "${i}" has non-array \`stories\`. Repair this ambiguous shape manually before invoking Neocortex.`,"epic_stories_not_array")}if(e.schema_version!==void 0&&e.schema_version!==0&&e.schema_version!==1)throw new S(`[Neocortex] state.json repair stopped: schema_version=${String(e.schema_version)} is not a safe automatic downgrade target. Run \`*migrate-state --preview\` or repair the schema version manually.`,"unsupported_schema_version");if(I(t.args,t.canonicalCommandId)){for(const i of["epics","stories"]){const p=e[i];if(p!==void 0){if(!d(p))throw new S("[Neocortex] inline materialization stopped: invalid state collection.","inline_state_collection_invalid");for(const[g,m]of Object.entries(p)){if(!d(m))throw new S("[Neocortex] inline materialization stopped: invalid state record.","inline_state_record_invalid");if(m.id!==void 0&&m.id!==g)throw new S("[Neocortex] inline materialization stopped: conflicting state identity.","inline_state_identity_conflict")}}}return{mutated:!1,state:e,reasonCodes:[],changes:[]}}let o;try{o=Z(e)}catch(i){throw i instanceof U?new S(`[Neocortex] state.json repair stopped by shared migration planner: ${i.message}`,i.reasonCode):i}let r=JSON.parse(JSON.stringify(o.kind==="migrate"?o.plan.targetState:e));const s=o.kind==="migrate"?o.plan.steps.map(i=>`shared migration ${i.kind}: ${i.target}`):[],c=o.kind==="migrate"?["state_migration_shared_plan"]:[];let l=[];const a=P(r,t.args,t.canonicalCommandId);if(we(r,s,c),be(r,s,c,a),typeof t.args=="string"){const i=me(r,{nowIso:t.nowIso,targetEpicId:a,effectiveDoneStoryIds:t.effectiveDoneStoryIds,preserveGitLifecycleAggregates:!0});if(i.mutated){r=i.state,l=i.changes;for(const p of i.reasonCodes)c.push(p);for(const p of i.changes)s.push(`reconciled aggregate for epic ${u(p.epicId)}`)}}const n=JSON.stringify(r)!==JSON.stringify(e);if(n){const i=t.nowIso??j(),p=Array.from(new Set(c));l.length>0&&je(r,l,i),r.last_updated=i,r.state_migration_repairs=[...Array.isArray(r.state_migration_repairs)?r.state_migration_repairs:[],{schemaVersion:1,action:"client_auto_repair",reasonCodes:p,repairedAt:i}],r.state_repair_debt=Ce(Array.isArray(r.state_repair_debt)?r.state_repair_debt:[],p,i)}return{mutated:n,state:r,reasonCodes:Array.from(new Set(c)),changes:s}}function G(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function L(e){const t=b(e);return t.length>0?C(t).slice(0,80):null}function Ae(e,t){return{schemaVersion:1,publicSafe:!0,epicId:u(e.epicId),reasonCodes:e.reasonCodes.map(C).filter(Boolean),previous:{total_stories:G(e.previous.total_stories),completed_stories:G(e.previous.completed_stories),status:L(e.previous.status),completed_at_present:typeof e.previous.completed_at=="string"&&e.previous.completed_at.length>0},next:{total_stories:e.next.total_stories,completed_stories:e.next.completed_stories,status:L(e.next.status),completed_at_present:typeof e.next.completed_at=="string"&&e.next.completed_at.length>0},repairedAt:t}}function xe(e,t,o){let r=e.filter(d).map(s=>({...s}));for(const s of t){const c=u(s.epicId),l=Array.from(new Set(s.reasonCodes.map(C).filter(Boolean))).sort(),a=`state-aggregate-reconciliation:${c}`,n={id:a,status:"open",reason:"epic_aggregate_reconciled",reason_codes:l,category:"state/epic-aggregate-reconciliation",epic_id:c,created_at:o,last_seen_at:o,occurrences:1},i=r.findIndex(g=>g.id===a);if(i<0){r=[...r,n];continue}const p=r[i];r[i]={...p,...n,created_at:typeof p.created_at=="string"?p.created_at:o,occurrences:Math.max(1,typeof p.occurrences=="number"?p.occurrences:1)+1}}return r}function je(e,t,o){e.state_aggregate_reconciliations=[...Array.isArray(e.state_aggregate_reconciliations)?e.state_aggregate_reconciliations.filter(d):[],...t.map(r=>Ae(r,o))],e.state_repair_debt=xe(Array.isArray(e.state_repair_debt)?e.state_repair_debt:[],t,o)}function Ce(e,t,o){const r=Array.from(new Set(t.map(C).filter(Boolean))).sort(),s=`state-drift-repair:${r.join("+")||"unknown"}`,c=e.filter(d).map(i=>({...i})),l={id:s,status:"open",reason:"state_json_drift_repaired",reason_codes:r,category:"state/drift-repair",created_at:o,last_seen_at:o,occurrences:1},a=c.findIndex(i=>i.id===s);if(a<0)return[...c,l];const n=c[a];return c[a]={...n,...l,created_at:typeof n.created_at=="string"?n.created_at:o,occurrences:Math.max(1,typeof n.occurrences=="number"?n.occurrences:1)+1},c}function C(e){return e.replace(/\b(sk|ghp|gho|github_pat|xoxb|AKIA)[A-Za-z0-9_-]{8,}\b/g,"[redacted]").replace(/(password|passwd|secret|token|api[_-]?key)\s*[:=]\s*[^,\s)]+/gi,"$1=[redacted]").replace(/https?:\/\/[^\s)]+/gi,"[url-redacted]").replace(/[^a-z0-9_+-]+/gi,"_").replace(/^_+|_+$/g,"").slice(0,120)}function u(e){return e.replace(/[^a-z0-9_.:-]+/gi,"_").replace(/^_+|_+$/g,"").slice(0,120)||"unknown"}function Te(e,t={}){const o=x(e,".neocortex"),r=x(o,"state.json");if(!F(r))return{repaired:!1,reasonCodes:[],changes:[]};let s;if(!I(t.args,t.canonicalCommandId))try{const _=Y(e);_&&(s=K(e,_))}catch(_){const y=_ instanceof Q?_.reasonCode:"state_migration_apply_failed";throw new S(`[Neocortex] state.json migration stopped before invoke: ${_ instanceof Error?_.message:String(_)}. No unverified state was used.`,y)}let c;try{c=JSON.parse(q(r,"utf8"))}catch(_){const y=_ instanceof Error?_.message:String(_);throw new S(`[Neocortex] state.json repair stopped: invalid JSON in .neocortex/state.json (${y}). No files were overwritten. Fix the JSON or restore a backup, then rerun.`,"invalid_json")}if(!d(c))throw new S("[Neocortex] state.json repair stopped: top-level state must be a JSON object. Restore a valid .neocortex/state.json before invoking Neocortex.","state_not_object");const l=P(c,t.args,t.canonicalCommandId),a=I(t.args,t.canonicalCommandId)?[]:Ie(e,c,l),n=ve(c,{...t,effectiveDoneStoryIds:a});if(!n.mutated)return s?{repaired:!0,reasonCodes:["state_migration_transaction_applied"],backupPath:s.backupHash?`.neocortex/state.json.migration-backup-${s.operationId}`:void 0,changes:[`shared state migration ${s.outcome}`]}:{repaired:!1,reasonCodes:[],changes:[]};J(o,{recursive:!0});const i=ne(),p=x(o,`state.json.backup-${i}`),g=x(o,`state.json.tmp.${V(6).toString("hex")}`);W(r,p),H(g,`${JSON.stringify(n.state,null,2)}
|
|
2
|
+
`,"utf8"),B(g,r);const m=`.neocortex/state.json.backup-${i}`;return process.stderr.write(`[Neocortex P156.05] state.json auto-repaired before invoke; backup=${m}; reasonCodes=${n.reasonCodes.join(",")}; changes=${n.changes.join("; ")}
|
|
3
|
+
`),{repaired:!0,reasonCodes:n.reasonCodes,backupPath:m,changes:n.changes}}function Ie(e,t,o){const r=X(e),s=d(t.epics)?t.epics:{},c=d(t.stories)?t.stories:{},l=[];for(const[a,n]of Object.entries(r.stories)){if(!d(n)||!T(n.status)||!n.checkpoint)continue;const i=d(c[a])?c[a]:null,p=typeof i?.epic_id=="string"?i.epic_id:a.split(".")[0];if(!p||o&&p!==o)continue;const g=d(s[p])?s[p]:null,m=d(r.epics[p])?r.epics[p]:null,_=m?.branch_mode??g?.branch_mode,y=m?.execution_branch??g?.execution_branch;if(_!=="epic"||typeof y!="string")continue;ee(e,n.checkpoint,a,p,y).verdict==="verified"&&l.push(a)}return l.sort()}export{f as EPIC_AGGREGATE_REASON_CODES,te as P189_STATE_SHAPE_GAP_CODE,S as StateRepairHardStopError,I as isInlineYoloMaterialization,T as isP189DoneLikeStoryStatus,Te as maybeRepairStateJsonBeforeInvoke,me as reconcileEpicAggregatesInState};
|
|
@@ -93,9 +93,8 @@ export declare function persistYoloopFromResponse(metadata: Record<string, unkno
|
|
|
93
93
|
* on crash mid-write. Mirrors `YoloopClientStateStore.writeState()`
|
|
94
94
|
* pattern.
|
|
95
95
|
*
|
|
96
|
-
* Migration filesystem failures
|
|
97
|
-
*
|
|
98
|
-
* invoke boundary can fail closed.
|
|
96
|
+
* Migration directives and filesystem failures return `blocking: true`; the
|
|
97
|
+
* invoke boundary cannot continue after an unverified state write or rollback.
|
|
99
98
|
*/
|
|
100
99
|
export interface StateWritebackResult {
|
|
101
100
|
readonly applied: boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import{YoloopClientStateStore as
|
|
2
|
-
`);return}}async function
|
|
3
|
-
`)}}function
|
|
4
|
-
`),{applied:!0,blocking:!1,reasonCode:"inline-yolo-materialized"}}catch(
|
|
5
|
-
`),{applied:!1,blocking:!0,reasonCode:"inline-yolo-persistence-failed"}}}async function
|
|
6
|
-
`),{applied
|
|
7
|
-
`),{applied:!
|
|
8
|
-
`),{applied:!1,blocking:!
|
|
1
|
+
import{statSync as F}from"node:fs";import{join as T}from"node:path";import{YoloopClientStateStore as j}from"./yoloop-client-state-store.js";import{isYoloopInvocation as M}from"./target-normalization.js";import{applyStateMigration as N,StateMigrationApplyError as V}from"../migrations/state-json.js";function H(e){return M(e)}const D=/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i;function Q(e){if(typeof e.args!="string"||e.args.length===0)return;const r=e.args.match(D);if(!r)return;const i=r[0];if(e.args.slice(i.length).trim()!=="")return;let a=!1;try{const o=T(e.projectRoot,"docs","epics");a=F(o).isDirectory()}catch(o){const c=o instanceof Error?o.message:String(o);e.logger?.warn?.(`[Neocortex] Zero-config *yoloop hook (P121.05): could not stat docs/epics/: ${c}. Proceeding with raw args.`);return}a&&(e.args=`${i} @docs/epics/`,e.logger?.info?.("[Neocortex] Zero-config *yoloop (P121.05): auto-resolved empty args to @docs/epics/"))}async function W(e,r,i){if(H(e))try{return await new j(r).readAll(i)}catch(t){process.stderr.write(`[yoloop] Failed to read local state: ${t.message}. Continuing without state.
|
|
2
|
+
`);return}}async function X(e,r,i){if(!e)return;const t=e.yoloopStateUpdate;if(!(!t||!Array.isArray(t.operations)||t.operations.length===0))try{await new j(r).applyUpdate(t,i)}catch(a){const o=a.message??String(a);process.stderr.write(`[yoloop] Failed to persist state: ${o}. 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 z(e,r){return Array.isArray(e)&&e.length<=r&&e.every(i=>typeof i=="string"&&i.length>0&&i.length<=160&&/^[A-Za-z0-9_.:+-]+$/.test(i))}function R(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 G(e){const r={...e};return delete r.scope,delete r.stateSummary,delete r.graphRetrieval,delete r.runnerBootstrap,delete r.projectEvidence,delete r.continuity,delete r.implementCheckpoint,r}function U(e){return typeof e=="string"&&["done","completed","merged"].includes(e.trim().toLowerCase())}async function K(e,r){const i=e.syntheticStory;if(!n(i))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-mutation-missing"};const t=i.storyId,a=i.storyPath,o=i.deterministicKey,c=i.storyContent;if(typeof t!="string"||!/^P\d+(?:\.\d+){2}$/.test(t)||typeof a!="string"||a!==`docs/stories/${t}.story.md`||typeof o!="string"||!/^[a-f0-9]{16}$/.test(o)||typeof c!="string"||c.length===0||c.length>131072||i.publicSafe!==!0||i.sanitized!==!0)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-mutation-invalid"};const d=await import("node:fs/promises"),u=await import("node:path"),O=await import("node:crypto"),b=u.join(r,".neocortex"),w=u.join(b,"state.json"),h=u.join(r,...a.split("/"));let I;try{I=await d.readFile(w,"utf8")}catch{return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-missing"}}let p;try{p=JSON.parse(I)}catch{return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-corrupt"}}if(!n(p)||!n(p.stories)||!n(p.epics)||R(p))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-state-not-full"};const s=p.stories[t];if(s!==void 0&&(!n(s)||s.epic_id!=="P174"||s.story_file!==a||!n(s.inlineYolo)||s.inlineYolo.deterministicKey!==o))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-conflict"};const g=p.epics.P174;if(g!==void 0&&(!n(g)||g.id!==void 0&&g.id!=="P174"||!Array.isArray(g.stories)))return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-conflict"};const k=n(g)?g:{},f=Array.isArray(k.stories)?k.stories:[];if(f.some(l=>typeof l!="string")||new Set(f).size!==f.length)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-story-list-invalid"};for(const l of f){const m=p.stories[l];if(!n(m)||m.id!==l||m.epic_id!=="P174")return{applied:!1,blocking:!0,reasonCode:"inline-yolo-epic-ownership-conflict"}}try{const l=await d.readFile(h,"utf8"),m=l.includes(`id: ${t}`)&&l.includes(`synthetic_key: ${o}`);if(l!==c&&!m)return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-file-conflict"}}catch(l){if(l.code!=="ENOENT")return{applied:!1,blocking:!0,reasonCode:"inline-yolo-story-file-unreadable"}}const y=f.includes(t)?[...f]:[...f,t],S={...p.stories,[t]:{...n(s)?s:{},id:t,epic_id:"P174",title:"Synthetic inline USER STORY for yolo",status:n(s)&&typeof s.status=="string"?s.status:"backlog",steps_completed:n(s)&&Array.isArray(s.steps_completed)?s.steps_completed:[],last_step:n(s)?s.last_step??null:null,branch_name:n(s)?s.branch_name??null:null,story_file:a,source:"inline-yolo-materialization",inlineYolo:{...n(s)&&n(s.inlineYolo)?s.inlineYolo:{},schemaVersion:1,deterministicKey:o,publicSafe:!0,sanitized:!0,materializedBeforeExecution:!0,ephemeralExecutionForbidden:!0}}},C=y.filter(l=>n(S[l])&&U(S[l].status)).length,P=n(p.inlineYolo)?p.inlineYolo:{},Y=n(P.materializations)?P.materializations:{},x=G({...p,stories:S,epics:{...p.epics,P174:{...k,id:"P174",status:C===y.length&&y.length>0?"done":"in-progress",stories:y,total_stories:y.length,completed_stories:C,...C===y.length&&y.length>0?{}:{completed_at:null}}},inlineYolo:{...P,schemaVersion:1,materializations:{...Y,[t]:{schemaVersion:1,storyId:t,storyPath:a,deterministicKey:o,publicSafe:!0,sanitized:!0,materializedBeforeExecution:!0,ephemeralExecutionForbidden:!0,pipelineTrigger:`*yolo @${a}`,reasonCodes:Array.isArray(i.reasonCodes)?i.reasonCodes:[]}}},last_updated:new Date().toISOString()}),_=`${Date.now()}-${O.randomBytes(6).toString("hex")}`,A=u.join(b,`state.json.tmp.${_}`),$=`${h}.tmp.${_}`,E=u.join(b,`state.json.backup-${new Date().toISOString().replace(/[:.]/g,"-")}`);let v=!1;try{await d.mkdir(b,{recursive:!0}),await d.mkdir(u.dirname(h),{recursive:!0});try{await d.access(h)}catch{await d.writeFile($,c,{encoding:"utf8",flag:"wx"}),await d.rename($,h),v=!0}return await d.copyFile(w,E),await d.writeFile(A,JSON.stringify(x,null,2),{encoding:"utf8",flag:"wx"}),await d.rename(A,w),process.stderr.write(`[Neocortex] inline yolo story materialized with bounded full-state merge; story=${t}; backup=${u.basename(E)}.
|
|
4
|
+
`),{applied:!0,blocking:!1,reasonCode:"inline-yolo-materialized"}}catch(l){await Promise.allSettled([d.unlink(A),d.unlink($)]),v&&await d.unlink(h).catch(()=>{});const m=l.message??String(l);return process.stderr.write(`[Neocortex] inline yolo materialization failed: ${m}.
|
|
5
|
+
`),{applied:!1,blocking:!0,reasonCode:"inline-yolo-persistence-failed"}}}async function ee(e,r){if(!e||e.stateMutated!==!0)return{applied:!1,blocking:!1,reasonCode:"state-writeback-not-requested"};if(e.stateMutationStrategy==="merge-inline-yolo-v1"||e.stateMutationReason==="inline-yolo-materialized-synthetic-story")return K(e,r);if(e.stateMutationStrategy==="state-migration-v1"||e.stateMigrationPlan!==void 0){const t=e.stateMigrationPlan,a=new Set(["schemaVersion","operationId","sourceSchemaVersion","targetSchemaVersion","sourceCanonicalHash","targetCanonicalHash","steps","migratedEpicIds","migratedStoryIds","preservedUnknownFieldCount","reasonCodes"]);if(!n(t)||Object.keys(t).some(o=>!a.has(o))||t.schemaVersion!=="StateMigrationPlan/1.0"||typeof t.operationId!="string"||!/^smig_[a-f0-9]{32}$/.test(t.operationId)||typeof t.sourceCanonicalHash!="string"||!/^[a-f0-9]{64}$/.test(t.sourceCanonicalHash)||typeof t.targetCanonicalHash!="string"||!/^[a-f0-9]{64}$/.test(t.targetCanonicalHash)||![0,1,"absent"].includes(t.sourceSchemaVersion)||t.targetSchemaVersion!==1||!Array.isArray(t.steps)||t.steps.length>8192||t.steps.some(o=>!n(o)||Object.keys(o).some(c=>!["kind","source","target"].includes(c))||!["initialize-collection","merge-epic","merge-story","preserve-legacy","remove-legacy","set-schema-version"].includes(String(o.kind))||typeof o.target!="string"||o.target.length>512||!/^[A-Za-z0-9_.:[\]-]+$/.test(o.target)||o.source!==void 0&&(typeof o.source!="string"||o.source.length>512||!/^[A-Za-z0-9_.:[\]-]+$/.test(o.source)))||!z(t.migratedEpicIds,4096)||!z(t.migratedStoryIds,4096)||!z(t.reasonCodes,32)||!Number.isSafeInteger(t.preservedUnknownFieldCount)||t.preservedUnknownFieldCount<0)return{applied:!1,blocking:!0,reasonCode:"state-migration-plan-missing-or-invalid"};try{const o=N(r,t),c=`.neocortex/state.json.migration-backup-${o.operationId}`;return process.stderr.write(`[P220-EV-STATE-MIGRATION] outcome=${o.outcome}; operation=${o.operationId}; backup=${o.backupHash?c:"none"}.
|
|
6
|
+
`),{applied:o.outcome==="applied",blocking:!1,reasonCode:o.outcome==="applied"?"state-migration-applied":"state-migration-replay-verified"}}catch(o){const c=o instanceof V?o.reasonCode:"state-migration-apply-failed";return process.stderr.write(`[P220-EV-STATE-MIGRATION] blocked=${c}.
|
|
7
|
+
`),{applied:!1,blocking:!0,reasonCode:c}}}const i=e.migratedState;return!i||typeof i!="object"||Array.isArray(i)?{applied:!1,blocking:!0,reasonCode:"state-writeback-payload-missing"}:R(i)?(process.stderr.write(`[Neocortex] Refused projected/partial state replacement; canonical state was not modified.
|
|
8
|
+
`),{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 te(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(r){const i=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook (lazy load) failed: ${i}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`lazy load error: ${i}`}}}async function oe(e){if(!e.featureFlag)return{applied:!1,reason:"flag_off"};try{return await(await import("../graph-retrieval/pre-command-hook.js")).runGraphRetrievalHook(e)}catch(r){const i=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] graph retrieval hook (lazy load) failed: ${i}. Proceeding without enrichment.`),{applied:!1,reason:"hook_error",warning:i}}}export{H as isYoloopTrigger,Q as maybeAutoResolveYoloopArgs,te as maybeRunDiscoveryHook,oe as maybeRunGraphRetrievalHook,ee as persistMigratedStateFromResponse,X as persistYoloopFromResponse,W as readYoloopForRequest};
|
|
@@ -271,9 +271,9 @@ export interface MergeResult {
|
|
|
271
271
|
* preserved). Critical for `/loop` iteration which may re-invoke discovery
|
|
272
272
|
* between stories.
|
|
273
273
|
*
|
|
274
|
-
* **Mutation semantics**:
|
|
275
|
-
*
|
|
276
|
-
*
|
|
274
|
+
* **Mutation semantics**: merge work is staged on cloned maps and committed to
|
|
275
|
+
* the caller-owned container only after every identity and aggregate invariant
|
|
276
|
+
* passes. Validation failures therefore provide strong exception safety.
|
|
277
277
|
*
|
|
278
278
|
* @param state target state object (`null`/`undefined` ok -- treated as empty)
|
|
279
279
|
* @param discovered discovery result (epics + warnings + skipped)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const me=Object.freeze({epicId:"P{number}",epicFilename:"docs/epics/epic-P{number}.md",storyId:"P{number}.NN",storyFilename:"docs/stories/P{number}.NN.story.md",acceptedOnly:Object.freeze({epicFilenames:Object.freeze(["docs/epics/epic-{number}.md","docs/epics/EPIC-{number}-{slug}.md","docs/epics/EPIC-{number}_{slug}.md","docs/epics/epic-{slug}.md"]),storyFilenames:Object.freeze(["docs/stories/{number}.NN.story.md","docs/stories/story-P{number}.NN.md","docs/stories/story-{number}.NN.md","docs/stories/EPIC-{number}.STORY-{number}.md"])})}),E=Object.freeze([/^epic-(P?\d+(?:\.\d+)?)\.md$/i,/^EPIC-(\d+)(?:[-_].*)?\.md$/,/^epic-([a-z0-9][a-z0-9_-]*)\.md$/i]),j=Object.freeze([/^(P?\d+\.\d+(?:\.\d+)?)\.story\.md$/i,/^story-(P?\d+\.\d+)\.md$/i,/^EPIC-(\d+)\.STORY-(\d+)\.md$/]),k=Object.freeze([".archive","_archive","_archives","_drafts","archive","archives","archived","drafts"]),A=/^---\s*\n([\s\S]*?)\n---/;function w(t,e){const i=new RegExp(`^${e}:\\s*(.+?)\\s*$`,"mi"),n=t.match(i);if(!n)return;let c=n[1].trim();return(c.startsWith('"')&&c.endsWith('"')||c.startsWith("'")&&c.endsWith("'"))&&(c=c.slice(1,-1)),c||void 0}function M(t){if(typeof t!="string"||t.length===0)return{};const e=t.match(A);if(!e)return{};const i=e[1];return{id:w(i,"id"),title:w(i,"title"),status:w(i,"status"),epic_id:w(i,"epic_id"),depends_on:Q(i),files_to_modify:R(i,"files_to_modify"),total_stories:D(w(i,"total_stories"))}}const C=32,G=4096,Y=/^\s*\*\*([^*\r\n]+?):\*\*\s*(.*?)\s*$/,B=new Set(["backlog","pending","ready","planning","in-progress","done","completed","merged","cancelled","canceled","abandoned","draft","template","archived","branch-ready","diagnose","research","write-spec","create-tasks","implemented","refactor-suggest-done","property-tests-emitted","tests-validated","qa-gate-passed","memory-updated","committed","synced","pr-created","pr-reviewed","partial"]);function W(t){let e=t.trim();return e.startsWith("`")&&e.endsWith("`")&&e.length>=2&&(e=e.slice(1,-1).trim()),(e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1).trim()),e}function D(t){if(t===void 0||!/^\d{1,5}$/.test(t.trim()))return;const e=Number(t);return Number.isSafeInteger(e)?e:void 0}function H(t){const e=W(t).toLowerCase().replace(/[_\s]+/g,"-"),n={planned:"planning",complete:"completed",closed:"done",finished:"done"}[e]??e;return B.has(n)?n:void 0}function U(t){const e=t.trim().toLowerCase().replace(/[_-]+/g," ").replace(/\s+/g," ");if(e==="id"||e==="epic id")return"id";if(e==="title"||e==="titulo")return"title";if(e==="status")return"status";if(e==="total stories"||e==="total de stories")return"total_stories"}function Z(t){const e=t.find(c=>/^#\s+\S/.test(c));if(!e)return;const i=e.replace(/^#\s+/,"").trim();return(i.replace(/^(?:Epic|Story)\s+\S+\s*(?:(?:--?|:)|[\u2013\u2014])\s*/i,"")||i).slice(0,200)}function v(t,e,i,n){const c=[...new Set(t.map(W).filter(Boolean))];if(c.length>1){n.push(`legacy metadata ambiguous for '${e}' in '${i}': conflicting values ignored`);return}return c[0]}function X(t,e){const i=[],n=[],c=t.slice(0,G).split(/\r?\n/,C);for(const d of c){if(n.length>0&&(/^\s*---\s*$/.test(d)||/^##\s+/.test(d)))break;n.push(d)}const o=new Map;for(const d of n){const g=d.match(Y);if(!g)continue;const m=U(g[1]);if(!m)continue;const u=o.get(m)??[];u.push(g[2]),o.set(m,u)}const l=v(o.get("id")??[],"id",e,i),f=v(o.get("title")??[],"title",e,i),h=v(o.get("status")??[],"status",e,i),_=v(o.get("total_stories")??[],"total_stories",e,i),p=h===void 0?void 0:H(h);h!==void 0&&p===void 0&&i.push(`legacy metadata status unrecognized in '${e}': value ignored`);const y=D(_);_!==void 0&&y===void 0&&i.push(`legacy metadata total_stories invalid in '${e}': value ignored`);const b=l&&/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(l)?l:void 0;l!==void 0&&b===void 0&&i.push(`legacy metadata id invalid in '${e}': value ignored`);const r=Z(n),s=f??r;return{metadata:{...b?{id:b}:{},...s?{title:s.slice(0,200)}:{},...p?{status:p}:{},...y!==void 0?{total_stories:y}:{}},source:"legacy-metadata",warnings:Object.freeze(i)}}function q(t){return/^[A-Za-z0-9._-]{1,160}$/.test(t)?t:"markdown-target.md"}function K(t){return new TextEncoder().encode(t).length}function V(t){let e=!1;for(const i of t.split(/\r?\n/)){if(!i.trim()||i.trimStart().startsWith("#"))continue;const n=i.match(/^\s+-\s+(.+?)\s*$/);if(n){if(!e)return!0;const l=n[1].trim();if(l.startsWith('"')&&!l.endsWith('"')||l.startsWith("'")&&!l.endsWith("'"))return!0;continue}if(/^\s+/.test(i))return!0;const c=i.match(/^[A-Za-z0-9_-]+:\s*(.*?)\s*$/);if(!c)return!0;const o=c[1].trim();if(e=o.length===0,o.startsWith('"')&&!o.endsWith('"')||o.startsWith("'")&&!o.endsWith("'"))return!0}return!1}function J(t,e,i){const n=q(e),c=t.match(A);if(!c)return Object.freeze([`metadata-frontmatter reason=frontmatter-malformed file='${n}': repair the bounded leading YAML envelope; source content omitted`]);const o=c[1];return K(o)>8*1024||o.split(/\r?\n/).length>128?Object.freeze([`metadata-frontmatter reason=frontmatter-oversized file='${n}': reduce the leading YAML envelope; source content omitted`]):V(o)||o.trim().length>0&&Object.keys(i).length===0?Object.freeze([`metadata-frontmatter reason=frontmatter-unsupported-value file='${n}': use bounded scalar fields or string lists; source content omitted`]):Object.freeze([])}function F(t,e){if(/^---(?:\s*\r?\n|\s*$)/.test(t)){const i=M(t);return{metadata:i,source:"frontmatter",warnings:J(t,e,i)}}return typeof t!="string"||t.length===0?{metadata:{},source:"none",warnings:Object.freeze([])}:X(t,e)}function L(t){let e=t.trim();if((e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1).trim()),!!e&&!/^(null|undefined|true|false)$/i.test(e))return e}function Q(t){return R(t,"depends_on")}function R(t,e){const i=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=t.match(new RegExp(`^${i}:[ \\t]*(.+?)[ \\t]*$`,"mi"))?.[1]?.trim();if(c!==void 0){const _=(c.startsWith("[")&&c.endsWith("]")?c.slice(1,-1).split(","):[c]).map(L).filter(p=>typeof p=="string"&&p.length>0);return _.length>0?Object.freeze(_):void 0}const o=t.split(/\r?\n/),l=[];let f=!1;for(const h of o){if(!f){new RegExp(`^${i}:\\s*$`,"i").test(h.trim())&&(f=!0);continue}if(/^[A-Za-z0-9_-]+:\s*/.test(h))break;const _=h.match(/^\s*-\s*(.+?)\s*$/);if(!_)continue;const p=L(_[1]);p&&l.push(p)}return l.length>0?Object.freeze(l):void 0}function N(t){return t.startsWith("_")||t.startsWith(".")}function ee(t){return k.includes(t)}async function ge(){const t=await import("node:fs"),e=await import("node:path");return{isDirectory(i){try{return t.statSync(i).isDirectory()}catch{return!1}},readdir(i){try{return t.readdirSync(i)}catch{return[]}},readFile(i){try{return t.readFileSync(i,"utf8")}catch{return""}},join(...i){try{return e.join(...i)}catch{return i.join("/")}}}}function te(t,e){let i=!1,n=null,c=null;for(let o=0;o<E.length;o++){const l=E[o],f=t.match(l);if(f){i=!0,n=f[1],c=o===2?"filename-slug":"filename-canonical";break}}return i?e.id&&e.id.length>0?{id:e.id,source:"frontmatter"}:{id:n,source:c}:null}function ie(t){for(const e of E){const i=t.match(e);if(i)return i[1]}return null}function O(t){const e=t.trim();return/^\d+(?:\.\d+)*$/i.test(e)?`P${e}`.toUpperCase():e.toUpperCase()}function ne(t){const e=O(t);return/^P\d+(?:\.\d+)?$/i.test(e)?`epic-${e}.md`:`epic-${t}.md`}function se(t){const e=O(t);return/^P\d+\.\d+(?:\.\d+)?$/i.test(e)?`${e}.story.md`:`${t}.story.md`}function P(t,e){return!t||!e?!1:O(t)!==O(e)}function re(t,e,i){return`frontmatter/filename mismatch: epic '${t}' has frontmatter id '${e}' but filename implies '${i}'. Repair by using generated canonical '${ne(e)}' or changing frontmatter id to '${i}'.`}function oe(t,e,i){return`frontmatter/filename mismatch: story '${t}' has frontmatter id '${e}' but filename implies '${i}'. Repair by using generated canonical '${se(e)}' or changing frontmatter id to '${i}'.`}function ce(t){let e=t.match(j[0]);if(e){const i=e[1],n=i.indexOf(".");return{id:i,epic_id_hint:n>0?i.slice(0,n):null}}if(e=t.match(j[1]),e){const i=e[1],n=i.indexOf(".");return{id:i,epic_id_hint:n>0?i.slice(0,n):null}}if(e=t.match(j[2]),e){const i=e[1],n=e[2];return{id:`${i}.${n}`,epic_id_hint:i}}return null}function ae(t,e){return e.join(t,"..","stories")}function he(t,e){const i=[],n=[],c=[],o=new Date().toISOString();if(typeof t!="string"||t.length===0)return Object.freeze({epics:Object.freeze([]),warnings:Object.freeze([]),skipped:Object.freeze([])});if(!e.isDirectory(t))return Object.freeze({epics:Object.freeze([]),warnings:Object.freeze([]),skipped:Object.freeze([])});const l=[...e.readdir(t)].sort(),f=[];for(const r of l){const s=e.join(t,r);if(N(r)){n.push(`${r}: ignored (draft or hidden prefix)`);continue}if(e.isDirectory(s)){if(ee(r)){n.push(`${r}: ignored subdirectory`);continue}n.push(`${r}: subdirectory not recursed`);continue}if(!r.endsWith(".md"))continue;if(!E.some(S=>S.test(r))){n.push(`${r}: did not match any EPIC_FILE_PATTERNS`);continue}const d=e.readFile(s),g=F(d,r);i.push(...g.warnings);const m=g.metadata,u=ie(r);m.id&&P(m.id,u)&&i.push(g.source==="frontmatter"?re(r,m.id,u):`legacy metadata/filename mismatch: epic '${r}' id does not match its filename`),f.push({filename:r,fullPath:s,metadata:m,metadataSource:g.source})}const h=new Map,_=[];for(const r of f){const s=te(r.filename,r.metadata);if(s===null){n.push(`${r.filename}: id resolution returned null`);continue}let a=s.id;const d=a,g=h.get(d)??0;g>0&&(a=`${d}-${g+1}`,i.push(`epic ID collision: '${d}' resolved to '${a}' for ${r.filename}`)),h.set(d,g+1),_.push({candidate:r,finalId:a,idSource:r.metadataSource==="legacy-metadata"&&r.metadata.id?"legacy-metadata":s.source})}const p=new Map,y=ae(t,e);if(e.isDirectory(y)){const r=[...e.readdir(y)].sort();for(const s of r){if(N(s)||!s.endsWith(".md"))continue;const a=e.join(y,s);if(e.isDirectory(a))continue;const d=ce(s);if(!d)continue;const g=e.readFile(a),m=F(g,s);i.push(...m.warnings);const u=m.metadata;u.id&&P(u.id,d.id)&&i.push(m.source==="frontmatter"?oe(s,u.id,d.id):`legacy metadata/filename mismatch: story '${s}' id does not match its filename`);const S=u.epic_id??d.epic_id_hint;if(!S){i.push(`story '${s}' has no epic_id (frontmatter or filename pattern)`);continue}const x=Object.freeze({id:d.id,epic_id:S,filename:s,path:a,...u.title?{title:u.title}:{},...u.status?{status:u.status}:{},...u.depends_on?{depends_on:u.depends_on}:{},...u.files_to_modify?{files_to_modify:u.files_to_modify}:{}});p.has(S)||p.set(S,[]),p.get(S).push(x)}}for(const r of _){const s=p.get(r.finalId)??[],a=Object.freeze({id:r.finalId,filename:r.candidate.filename,path:r.candidate.fullPath,...r.candidate.metadata.title?{title:r.candidate.metadata.title}:{},...r.candidate.metadata.status?{status:r.candidate.metadata.status}:{},...r.candidate.metadata.total_stories!==void 0?{total_stories:r.candidate.metadata.total_stories}:{},...r.candidate.metadata.depends_on?{depends_on:r.candidate.metadata.depends_on}:{},source:"discovered",discovered_at:o,stories:Object.freeze([...s]),t:r.idSource,e:r.candidate.metadataSource});c.push(a)}const b=new Set(c.map(r=>r.id));for(const[r,s]of p.entries())if(!b.has(r))for(const a of s)i.push(`orphan story '${a.filename}' references unknown epic '${r}'`);return Object.freeze({epics:Object.freeze(c),warnings:Object.freeze(i),skipped:Object.freeze(n)})}function de(t){return I(t)}function I(t){const e=ue(t);return e==="done"||e==="completed"||e==="merged"}function z(t){return I(t)}function ue(t){return typeof t=="string"?t.trim().toLowerCase().replace(/[_\s]+/g,"-"):""}function T(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}function $(t){throw new Error(`epic-aggregate-writer:${t}`)}function fe(t){const e=[],i=new Set;for(const n of t)(typeof n!="string"||n.length===0)&&$("epic_aggregate_story_ref_invalid"),i.has(n)&&$("epic_aggregate_story_ref_duplicate"),i.add(n),e.push(n);return e}function le(t,e,i){const n=i[e];return n===void 0&&$("epic_aggregate_story_missing"),T(n)||$("epic_aggregate_story_not_object"),n.id!==e&&$("epic_aggregate_story_id_conflict"),n.epic_id!==t&&$("epic_aggregate_story_epic_conflict"),n}function pe(t,e,i,n,c){const o=e[t];T(o)||$("epic_aggregate_epic_not_object"),o.id!==t&&$("epic_aggregate_epic_id_conflict"),Array.isArray(o.stories)||$("epic_aggregate_story_list_not_array");const l=fe(o.stories),f=l.length,h=l.filter(s=>I(le(t,s,i).status)).length,_=c?.total_stories,p=c?.e==="legacy-metadata"&&z(o.status),y=p?_??f:f>0?f:_??0,b=p||f===0&&y>0&&z(o.status)?y:h,r=f>0&&h===f;z(o.status)&&!p&&!r&&f>0&&$("epic_aggregate_terminal_incomplete_conflict"),o.total_stories=y,o.completed_stories=b,!z(o.status)&&r&&(o.status="done",(o.completed_at===void 0||o.completed_at===null)&&(o.completed_at=n))}function _e(t,e,i){const n=t??{};(!n.epics||typeof n.epics!="object")&&(n.epics={}),(!n.stories||typeof n.stories!="object")&&(n.stories={});const c=n.epics,o=n.stories,l=[],f=[],h=new Set,_=new Map;let p=0;const y=i?.forceOverwrite===!0,b=i?.nowIso??new Date().toISOString(),r=new Set;for(const s of e.epics)r.add(s.id),_.set(s.id,s);for(const s of e.epics){const a=c[s.id],d=a;if(d&&(d.branch_mode==="epic"||typeof d.execution_branch=="string")){f.push({id:s.id,reason:"epic Git lifecycle preserved"});continue}if(a&&a.source==="create-epic"&&!y){f.push({id:s.id,reason:"create-epic source preserved (defense)"});continue}if(a&&a.source==="discovered"&&!y){const m=a;m.depends_on=s.depends_on??[],m.stories=s.stories.map(u=>u.id),s.title!==void 0&&(m.title=s.title),s.status!==void 0&&!z(m.status)&&(m.status=s.status),h.add(s.id);continue}const g={id:s.id,title:s.title??"",status:s.status??"planning",source:"discovered",discovered_at:b,merged_at:b,stories:s.stories.map(m=>m.id),depends_on:s.depends_on??[]};c[s.id]=g,h.add(s.id),l.push(s.id)}for(const s of e.epics)for(const a of s.stories){const d=o[a.id],g=c[s.id];if(d&&g&&(g.branch_mode==="epic"||typeof g.execution_branch=="string")){f.push({id:a.id,reason:"story Git lifecycle preserved"});continue}if(d&&d.source==="create-epic"&&!y){f.push({id:a.id,reason:"create-epic source preserved (defense, story)"});continue}if(d&&de(d.status)&&!y){const u=d;u.depends_on=a.depends_on??[],u.files_to_modify=a.files_to_modify??[];continue}if(d&&d.source==="discovered"&&!y){const u=d;u.depends_on=a.depends_on??[],u.files_to_modify=a.files_to_modify??[];continue}const m={id:a.id,epic_id:a.epic_id,title:a.title??"",status:a.status??"backlog",source:"discovered",discovered_at:b,depends_on:a.depends_on??[],files_to_modify:a.files_to_modify??[]};o[a.id]=m,p++}for(const s of h)pe(s,c,o,b,_.get(s));return{state:n,added:Object.freeze([...l]),skipped:Object.freeze([...f]),storiesAdded:p}}export{E as EPIC_FILE_PATTERNS,me as GENERATED_NAMING_CONTRACT,j as STORY_FILE_PATTERNS,ge as buildDefaultDiscoveryFs,he as discoverEpicsFromDir,te as getEpicIdFromDescriptor,_e as mergeDiscoveredIntoState};
|
|
1
|
+
const ge=Object.freeze({epicId:"P{number}",epicFilename:"docs/epics/epic-P{number}.md",storyId:"P{number}.NN",storyFilename:"docs/stories/P{number}.NN.story.md",acceptedOnly:Object.freeze({epicFilenames:Object.freeze(["docs/epics/epic-{number}.md","docs/epics/EPIC-{number}-{slug}.md","docs/epics/EPIC-{number}_{slug}.md","docs/epics/epic-{slug}.md"]),storyFilenames:Object.freeze(["docs/stories/{number}.NN.story.md","docs/stories/story-P{number}.NN.md","docs/stories/story-{number}.NN.md","docs/stories/EPIC-{number}.STORY-{number}.md"])})}),O=Object.freeze([/^epic-(P?\d+(?:\.\d+)?)\.md$/i,/^EPIC-(\d+)(?:[-_].*)?\.md$/,/^epic-([a-z0-9][a-z0-9_-]*)\.md$/i]),D=Object.freeze([/^(P?\d+\.\d+(?:\.\d+)?)\.story\.md$/i,/^story-(P?\d+\.\d+)\.md$/i,/^EPIC-(\d+)\.STORY-(\d+)\.md$/]),k=Object.freeze([".archive","_archive","_archives","_drafts","archive","archives","archived","drafts"]),F=/^---\s*\n([\s\S]*?)\n---/;function z(t,e){const i=new RegExp(`^${e}:\\s*(.+?)\\s*$`,"mi"),n=t.match(i);if(!n)return;let c=n[1].trim();return(c.startsWith('"')&&c.endsWith('"')||c.startsWith("'")&&c.endsWith("'"))&&(c=c.slice(1,-1)),c||void 0}function G(t){if(typeof t!="string"||t.length===0)return{};const e=t.match(F);if(!e)return{};const i=e[1];return{id:z(i,"id"),title:z(i,"title"),status:z(i,"status"),epic_id:z(i,"epic_id"),depends_on:te(i),files_to_modify:P(i,"files_to_modify"),total_stories:x(z(i,"total_stories"))}}const Y=32,B=4096,H=/^\s*\*\*([^*\r\n]+?):\*\*\s*(.*?)\s*$/,U=new Set(["backlog","pending","ready","planning","in-progress","done","completed","merged","cancelled","canceled","abandoned","draft","template","archived","branch-ready","diagnose","research","write-spec","create-tasks","implemented","refactor-suggest-done","property-tests-emitted","tests-validated","qa-gate-passed","memory-updated","committed","synced","pr-created","pr-reviewed","partial"]);function L(t){let e=t.trim();return e.startsWith("`")&&e.endsWith("`")&&e.length>=2&&(e=e.slice(1,-1).trim()),(e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1).trim()),e}function x(t){if(t===void 0||!/^\d{1,5}$/.test(t.trim()))return;const e=Number(t);return Number.isSafeInteger(e)?e:void 0}function Z(t){const e=L(t).toLowerCase().replace(/[_\s]+/g,"-"),n={planned:"planning",complete:"completed",closed:"done",finished:"done"}[e]??e;return U.has(n)?n:void 0}function X(t){const e=t.trim().toLowerCase().replace(/[_-]+/g," ").replace(/\s+/g," ");if(e==="id"||e==="epic id")return"id";if(e==="title"||e==="titulo")return"title";if(e==="status")return"status";if(e==="total stories"||e==="total de stories")return"total_stories"}function q(t){const e=t.find(c=>/^#\s+\S/.test(c));if(!e)return;const i=e.replace(/^#\s+/,"").trim();return(i.replace(/^(?:Epic|Story)\s+\S+\s*(?:(?:--?|:)|[\u2013\u2014])\s*/i,"")||i).slice(0,200)}function j(t,e,i,n){const c=[...new Set(t.map(L).filter(Boolean))];if(c.length>1){n.push(`legacy metadata ambiguous for '${e}' in '${i}': conflicting values ignored`);return}return c[0]}function K(t,e){const i=[],n=[],c=t.slice(0,B).split(/\r?\n/,Y);for(const m of c){if(n.length>0&&(/^\s*---\s*$/.test(m)||/^##\s+/.test(m)))break;n.push(m)}const o=new Map;for(const m of n){const r=m.match(H);if(!r)continue;const a=X(r[1]);if(!a)continue;const d=o.get(a)??[];d.push(r[2]),o.set(a,d)}const f=j(o.get("id")??[],"id",e,i),l=j(o.get("title")??[],"title",e,i),_=j(o.get("status")??[],"status",e,i),g=j(o.get("total_stories")??[],"total_stories",e,i),p=_===void 0?void 0:Z(_);_!==void 0&&p===void 0&&i.push(`legacy metadata status unrecognized in '${e}': value ignored`);const b=x(g);g!==void 0&&b===void 0&&i.push(`legacy metadata total_stories invalid in '${e}': value ignored`);const $=f&&/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(f)?f:void 0;f!==void 0&&$===void 0&&i.push(`legacy metadata id invalid in '${e}': value ignored`);const s=q(n),u=l??s;return{metadata:{...$?{id:$}:{},...u?{title:u.slice(0,200)}:{},...p?{status:p}:{},...b!==void 0?{total_stories:b}:{}},source:"legacy-metadata",warnings:Object.freeze(i)}}function V(t){return/^[A-Za-z0-9._-]{1,160}$/.test(t)?t:"markdown-target.md"}function J(t){return new TextEncoder().encode(t).length}function Q(t){let e=!1;for(const i of t.split(/\r?\n/)){if(!i.trim()||i.trimStart().startsWith("#"))continue;const n=i.match(/^\s+-\s+(.+?)\s*$/);if(n){if(!e)return!0;const f=n[1].trim();if(f.startsWith('"')&&!f.endsWith('"')||f.startsWith("'")&&!f.endsWith("'"))return!0;continue}if(/^\s+/.test(i))return!0;const c=i.match(/^[A-Za-z0-9_-]+:\s*(.*?)\s*$/);if(!c)return!0;const o=c[1].trim();if(e=o.length===0,o.startsWith('"')&&!o.endsWith('"')||o.startsWith("'")&&!o.endsWith("'"))return!0}return!1}function ee(t,e,i){const n=V(e),c=t.match(F);if(!c)return Object.freeze([`metadata-frontmatter reason=frontmatter-malformed file='${n}': repair the bounded leading YAML envelope; source content omitted`]);const o=c[1];return J(o)>8*1024||o.split(/\r?\n/).length>128?Object.freeze([`metadata-frontmatter reason=frontmatter-oversized file='${n}': reduce the leading YAML envelope; source content omitted`]):Q(o)||o.trim().length>0&&Object.keys(i).length===0?Object.freeze([`metadata-frontmatter reason=frontmatter-unsupported-value file='${n}': use bounded scalar fields or string lists; source content omitted`]):Object.freeze([])}function N(t,e){if(/^---(?:\s*\r?\n|\s*$)/.test(t)){const i=G(t);return{metadata:i,source:"frontmatter",warnings:ee(t,e,i)}}return typeof t!="string"||t.length===0?{metadata:{},source:"none",warnings:Object.freeze([])}:K(t,e)}function R(t){let e=t.trim();if((e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1).trim()),!!e&&!/^(null|undefined|true|false)$/i.test(e))return e}function te(t){return P(t,"depends_on")}function P(t,e){const i=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),c=t.match(new RegExp(`^${i}:[ \\t]*(.+?)[ \\t]*$`,"mi"))?.[1]?.trim();if(c!==void 0){const g=(c.startsWith("[")&&c.endsWith("]")?c.slice(1,-1).split(","):[c]).map(R).filter(p=>typeof p=="string"&&p.length>0);return g.length>0?Object.freeze(g):void 0}const o=t.split(/\r?\n/),f=[];let l=!1;for(const _ of o){if(!l){new RegExp(`^${i}:\\s*$`,"i").test(_.trim())&&(l=!0);continue}if(/^[A-Za-z0-9_-]+:\s*/.test(_))break;const g=_.match(/^\s*-\s*(.+?)\s*$/);if(!g)continue;const p=R(g[1]);p&&f.push(p)}return f.length>0?Object.freeze(f):void 0}function T(t){return t.startsWith("_")||t.startsWith(".")}function ie(t){return k.includes(t)}async function he(){const t=await import("node:fs"),e=await import("node:path");return{isDirectory(i){try{return t.statSync(i).isDirectory()}catch{return!1}},readdir(i){try{return t.readdirSync(i)}catch{return[]}},readFile(i){try{return t.readFileSync(i,"utf8")}catch{return""}},join(...i){try{return e.join(...i)}catch{return i.join("/")}}}}function ne(t,e){let i=!1,n=null,c=null;for(let o=0;o<O.length;o++){const f=O[o],l=t.match(f);if(l){i=!0,n=l[1],c=o===2?"filename-slug":"filename-canonical";break}}return i?e.id&&e.id.length>0?{id:e.id,source:"frontmatter"}:{id:n,source:c}:null}function se(t){for(const e of O){const i=t.match(e);if(i)return i[1]}return null}function I(t){const e=t.trim();return/^\d+(?:\.\d+)*$/i.test(e)?`P${e}`.toUpperCase():e.toUpperCase()}function re(t){const e=I(t);return/^P\d+(?:\.\d+)?$/i.test(e)?`epic-${e}.md`:`epic-${t}.md`}function oe(t){const e=I(t);return/^P\d+\.\d+(?:\.\d+)?$/i.test(e)?`${e}.story.md`:`${t}.story.md`}function C(t,e){return!t||!e?!1:I(t)!==I(e)}function ce(t,e,i){return`frontmatter/filename mismatch: epic '${t}' has frontmatter id '${e}' but filename implies '${i}'. Repair by using generated canonical '${re(e)}' or changing frontmatter id to '${i}'.`}function ae(t,e,i){return`frontmatter/filename mismatch: story '${t}' has frontmatter id '${e}' but filename implies '${i}'. Repair by using generated canonical '${oe(e)}' or changing frontmatter id to '${i}'.`}function de(t){let e=t.match(D[0]);if(e){const i=e[1],n=i.indexOf(".");return{id:i,epic_id_hint:n>0?i.slice(0,n):null}}if(e=t.match(D[1]),e){const i=e[1],n=i.indexOf(".");return{id:i,epic_id_hint:n>0?i.slice(0,n):null}}if(e=t.match(D[2]),e){const i=e[1],n=e[2];return{id:`${i}.${n}`,epic_id_hint:i}}return null}function ue(t,e){return e.join(t,"..","stories")}function _e(t,e){const i=[],n=[],c=[],o=new Date().toISOString();if(typeof t!="string"||t.length===0)return Object.freeze({epics:Object.freeze([]),warnings:Object.freeze([]),skipped:Object.freeze([])});if(!e.isDirectory(t))return Object.freeze({epics:Object.freeze([]),warnings:Object.freeze([]),skipped:Object.freeze([])});const f=[...e.readdir(t)].sort(),l=[];for(const s of f){const u=e.join(t,s);if(T(s)){n.push(`${s}: ignored (draft or hidden prefix)`);continue}if(e.isDirectory(u)){if(ie(s)){n.push(`${s}: ignored subdirectory`);continue}n.push(`${s}: subdirectory not recursed`);continue}if(!s.endsWith(".md"))continue;if(!O.some(h=>h.test(s))){n.push(`${s}: did not match any EPIC_FILE_PATTERNS`);continue}const m=e.readFile(u),r=N(m,s);i.push(...r.warnings);const a=r.metadata,d=se(s);a.id&&C(a.id,d)&&i.push(r.source==="frontmatter"?ce(s,a.id,d):`legacy metadata/filename mismatch: epic '${s}' id does not match its filename`),l.push({filename:s,fullPath:u,metadata:a,metadataSource:r.source})}const _=new Map,g=[];for(const s of l){const u=ne(s.filename,s.metadata);if(u===null){n.push(`${s.filename}: id resolution returned null`);continue}let y=u.id;const m=y,r=_.get(m)??0;r>0&&(y=`${m}-${r+1}`,i.push(`epic ID collision: '${m}' resolved to '${y}' for ${s.filename}`)),_.set(m,r+1),g.push({candidate:s,finalId:y,idSource:s.metadataSource==="legacy-metadata"&&s.metadata.id?"legacy-metadata":u.source})}const p=new Map,b=ue(t,e);if(e.isDirectory(b)){const s=[...e.readdir(b)].sort();for(const u of s){if(T(u)||!u.endsWith(".md"))continue;const y=e.join(b,u);if(e.isDirectory(y))continue;const m=de(u);if(!m)continue;const r=e.readFile(y),a=N(r,u);i.push(...a.warnings);const d=a.metadata;d.id&&C(d.id,m.id)&&i.push(a.source==="frontmatter"?ae(u,d.id,m.id):`legacy metadata/filename mismatch: story '${u}' id does not match its filename`);const h=d.epic_id??m.epic_id_hint;if(!h){i.push(`story '${u}' has no epic_id (frontmatter or filename pattern)`);continue}const S=Object.freeze({id:m.id,epic_id:h,filename:u,path:y,...d.title?{title:d.title}:{},...d.status?{status:d.status}:{},...d.depends_on?{depends_on:d.depends_on}:{},...d.files_to_modify?{files_to_modify:d.files_to_modify}:{}});p.has(h)||p.set(h,[]),p.get(h).push(S)}}for(const s of g){const u=p.get(s.finalId)??[],y=Object.freeze({id:s.finalId,filename:s.candidate.filename,path:s.candidate.fullPath,...s.candidate.metadata.title?{title:s.candidate.metadata.title}:{},...s.candidate.metadata.status?{status:s.candidate.metadata.status}:{},...s.candidate.metadata.total_stories!==void 0?{total_stories:s.candidate.metadata.total_stories}:{},...s.candidate.metadata.depends_on?{depends_on:s.candidate.metadata.depends_on}:{},source:"discovered",discovered_at:o,stories:Object.freeze([...u]),t:s.idSource,e:s.candidate.metadataSource});c.push(y)}const $=new Set(c.map(s=>s.id));for(const[s,u]of p.entries())if(!$.has(s))for(const y of u)i.push(`orphan story '${y.filename}' references unknown epic '${s}'`);return Object.freeze({epics:Object.freeze(c),warnings:Object.freeze(i),skipped:Object.freeze(n)})}function M(t){const e=fe(t);return e==="done"||e==="completed"||e==="merged"}function A(t){return M(t)}function fe(t){return typeof t=="string"?t.trim().toLowerCase().replace(/[_\s]+/g,"-"):""}function E(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}function w(t){throw new Error(`epic-aggregate-writer:${t}`)}function le(t){const e=[],i=new Set;for(const n of t)(typeof n!="string"||n.length===0)&&w("epic_aggregate_story_ref_invalid"),i.has(n)&&w("epic_aggregate_story_ref_duplicate"),i.add(n),e.push(n);return e}function pe(t,e,i){const n=i[e];return n===void 0&&w("epic_aggregate_story_missing"),E(n)||w("epic_aggregate_story_not_object"),n.id!==e&&w("epic_aggregate_story_id_conflict"),n.epic_id!==t&&w("epic_aggregate_story_epic_conflict"),n}function me(t,e,i,n,c){const o=e[t];E(o)||w("epic_aggregate_epic_not_object"),o.id!==t&&w("epic_aggregate_epic_id_conflict"),Array.isArray(o.stories)||w("epic_aggregate_story_list_not_array");const f=le(o.stories),l=f.length,_=f.filter(u=>M(pe(t,u,i).status)).length,g=c?.total_stories,p=c?.e==="legacy-metadata"&&A(o.status),b=p?g??l:l>0?l:g??0,$=p||l===0&&b>0&&A(o.status)?b:_,s=l>0&&_===l;A(o.status)&&!p&&!s&&l>0&&w("epic_aggregate_terminal_incomplete_conflict"),o.total_stories=b,o.completed_stories=$,!A(o.status)&&s&&(o.status="done",(o.completed_at===void 0||o.completed_at===null)&&(o.completed_at=n))}function ye(t,e,i){const n=t??{},c=E(n.epics)?n.epics:{},o=E(n.stories)?n.stories:{},f=structuredClone(c),l=structuredClone(o),_=[],g=[],p=new Set,b=new Map;let $=0;const s=i?.forceOverwrite===!0,u=i?.nowIso??new Date().toISOString(),y=new Set,m=new Map;for(const r of e.epics){if(!r.id||y.has(r.id))throw new Error("discovery_epic_identity_conflict");y.add(r.id),b.set(r.id,r);const a=f[r.id];if(a!==void 0&&(!E(a)||a.id!==void 0&&a.id!==r.id))throw new Error("discovery_epic_identity_conflict");for(const d of r.stories){if(!d.id||d.epic_id!==r.id||m.has(d.id))throw new Error("discovery_story_identity_conflict");m.set(d.id,r.id);const h=l[d.id];if(h!==void 0&&(!E(h)||h.id!==void 0&&h.id!==d.id||h.epic_id!==void 0&&h.epic_id!==r.id))throw new Error("discovery_story_identity_conflict")}}for(const r of e.epics){const a=f[r.id],d=a;if(d&&(d.branch_mode==="epic"||typeof d.execution_branch=="string")){g.push({id:r.id,reason:"epic Git lifecycle preserved"});continue}if(a&&a.source==="create-epic"&&!s){g.push({id:r.id,reason:"create-epic source preserved (defense)"});continue}if(a){const S=a;S.depends_on=r.depends_on??[];const v=Array.isArray(S.stories)?S.stories.filter(W=>typeof W=="string"):[];S.stories=[...new Set([...v,...r.stories.map(W=>W.id)])],r.title!==void 0&&(S.title=r.title),a.source!=="create-epic"&&p.add(r.id);continue}const h={id:r.id,title:r.title??"",status:r.status??"planning",source:"discovered",discovered_at:u,merged_at:u,stories:r.stories.map(S=>S.id),depends_on:r.depends_on??[]};f[r.id]=h,p.add(r.id),_.push(r.id)}for(const r of e.epics)for(const a of r.stories){const d=l[a.id],h=f[r.id];if(d&&h&&(h.branch_mode==="epic"||typeof h.execution_branch=="string")){g.push({id:a.id,reason:"story Git lifecycle preserved"});continue}if(d&&d.source==="create-epic"&&!s){g.push({id:a.id,reason:"create-epic source preserved (defense, story)"});continue}if(d){const v=d;a.title!==void 0&&(v.title=a.title),v.depends_on=a.depends_on??[],v.files_to_modify=a.files_to_modify??[];continue}const S={id:a.id,epic_id:a.epic_id,title:a.title??"",status:"backlog",source:"discovered",discovered_at:u,depends_on:a.depends_on??[],files_to_modify:a.files_to_modify??[]};l[a.id]=S,$++}for(const r of p)me(r,f,l,u,b.get(r));return n.epics=f,n.stories=l,{state:n,added:Object.freeze([..._]),skipped:Object.freeze([...g]),storiesAdded:$}}export{O as EPIC_FILE_PATTERNS,ge as GENERATED_NAMING_CONTRACT,D as STORY_FILE_PATTERNS,he as buildDefaultDiscoveryFs,_e as discoverEpicsFromDir,ne as getEpicIdFromDescriptor,ye as mergeDiscoveredIntoState};
|
|
@@ -81,6 +81,78 @@ export interface YoloopPersistedSession {
|
|
|
81
81
|
/** P96.05 AC5: storyIds skipped due to `*yolo` timeout. */
|
|
82
82
|
readonly skippedStories?: readonly string[];
|
|
83
83
|
}
|
|
84
|
+
/** P220.11 additive run-control wire. Kept structural for standalone clients. */
|
|
85
|
+
export type YoloopRunStatus = 'created' | 'running' | 'pausing' | 'paused' | 'resuming' | 'cancelling' | 'success' | 'partial' | 'blocked' | 'failure' | 'exhausted' | 'cancelled';
|
|
86
|
+
export type YoloopOperationalBudgetDimension = 'deadline' | 'wall' | 'turns' | 'tokens' | 'cost' | 'toolCalls' | 'attempts' | 'feedback' | 'depth' | 'concurrency' | 'risk';
|
|
87
|
+
export type YoloopOperationalBudgetValues = Readonly<Record<YoloopOperationalBudgetDimension, number>>;
|
|
88
|
+
export interface YoloopPersistedOperationalBudgetV1 {
|
|
89
|
+
readonly schemaVersion: 'YoloopOperationalBudget/1.0';
|
|
90
|
+
readonly startedAtMs: number;
|
|
91
|
+
readonly limits: YoloopOperationalBudgetValues;
|
|
92
|
+
}
|
|
93
|
+
export interface YoloopPersistedProgressSnapshotV1 {
|
|
94
|
+
readonly schemaVersion: 'ProgressSnapshot/1.0';
|
|
95
|
+
readonly runId: string;
|
|
96
|
+
readonly snapshotId: string;
|
|
97
|
+
readonly sequence: number;
|
|
98
|
+
readonly revision: number;
|
|
99
|
+
readonly operationId: string;
|
|
100
|
+
readonly boundary: 'budget' | 'retry' | 'pause' | 'resume' | 'cancel' | 'terminal';
|
|
101
|
+
readonly phase: 'before' | 'after';
|
|
102
|
+
readonly status: YoloopRunStatus;
|
|
103
|
+
readonly occurredAt: string;
|
|
104
|
+
readonly parentSnapshotHash: string | null;
|
|
105
|
+
readonly snapshotHash: string;
|
|
106
|
+
readonly budget: {
|
|
107
|
+
readonly schemaVersion: 'YoloopOperationalBudgetSummary/1.0';
|
|
108
|
+
readonly limits: YoloopOperationalBudgetValues;
|
|
109
|
+
readonly usage: YoloopOperationalBudgetValues;
|
|
110
|
+
readonly exhaustedDimension: YoloopOperationalBudgetDimension | null;
|
|
111
|
+
};
|
|
112
|
+
readonly reasonCodes: readonly string[];
|
|
113
|
+
readonly publicSafe: true;
|
|
114
|
+
}
|
|
115
|
+
export interface YoloopPersistedRunEventV1 {
|
|
116
|
+
readonly schemaVersion: 'YoloopRunEvent/1.0';
|
|
117
|
+
readonly runId: string;
|
|
118
|
+
readonly operationId: string;
|
|
119
|
+
readonly eventId: string;
|
|
120
|
+
readonly kind: 'transition' | 'budget' | 'retry' | 'intent' | 'conflict' | 'terminal';
|
|
121
|
+
readonly sequence: number;
|
|
122
|
+
readonly revision: number;
|
|
123
|
+
readonly occurredAt: string;
|
|
124
|
+
readonly fromStatus: YoloopRunStatus;
|
|
125
|
+
readonly toStatus: YoloopRunStatus;
|
|
126
|
+
readonly reasonCodes: readonly string[];
|
|
127
|
+
readonly budgetDimension: YoloopOperationalBudgetDimension | null;
|
|
128
|
+
readonly parentEventHash: string | null;
|
|
129
|
+
readonly eventHash: string;
|
|
130
|
+
readonly publicSafe: true;
|
|
131
|
+
}
|
|
132
|
+
export interface YoloopRunTransitionReceiptV1 {
|
|
133
|
+
readonly schemaVersion: 'YoloopRunTransitionReceipt/1.0';
|
|
134
|
+
readonly runId: string;
|
|
135
|
+
readonly operationId: string;
|
|
136
|
+
readonly idempotencyKey: string;
|
|
137
|
+
readonly payloadHash: string;
|
|
138
|
+
readonly revision: number;
|
|
139
|
+
readonly snapshotHash: string | null;
|
|
140
|
+
readonly status: YoloopRunStatus;
|
|
141
|
+
}
|
|
142
|
+
export interface YoloopRunControlV1 {
|
|
143
|
+
readonly schemaVersion: 'YoloopRunControl/1.0';
|
|
144
|
+
readonly runId: string;
|
|
145
|
+
readonly revision: number;
|
|
146
|
+
readonly status: YoloopRunStatus;
|
|
147
|
+
readonly startedAt: string;
|
|
148
|
+
readonly deadlineAt: string;
|
|
149
|
+
readonly operationalBudget: YoloopPersistedOperationalBudgetV1;
|
|
150
|
+
readonly usage: YoloopOperationalBudgetValues;
|
|
151
|
+
readonly events: readonly YoloopPersistedRunEventV1[];
|
|
152
|
+
readonly progressSnapshots: readonly YoloopPersistedProgressSnapshotV1[];
|
|
153
|
+
readonly latestSnapshotHash: string | null;
|
|
154
|
+
readonly receipts: readonly YoloopRunTransitionReceiptV1[];
|
|
155
|
+
}
|
|
84
156
|
/**
|
|
85
157
|
* Forward-declared opaque reference to `YoloopRecoveryAttempt` (canonical lives
|
|
86
158
|
* in `@neocortex/shared/yoloop/recovery-types`). Structural mirror to avoid
|
|
@@ -100,7 +172,7 @@ export interface YoloopRecoveryAttemptRef {
|
|
|
100
172
|
* Mirrors server-internal `YoloopRuntimeState` minus lock-related fields.
|
|
101
173
|
*/
|
|
102
174
|
export interface YoloopPersistedCounters {
|
|
103
|
-
readonly version: '1.0' | '1.1';
|
|
175
|
+
readonly version: '1.0' | '1.1' | '1.2';
|
|
104
176
|
readonly lastResetAt: string;
|
|
105
177
|
readonly storiesRunToday: number;
|
|
106
178
|
readonly iterationsToday: number;
|
|
@@ -111,6 +183,8 @@ export interface YoloopPersistedCounters {
|
|
|
111
183
|
readonly totalRunsAllTime: number;
|
|
112
184
|
readonly totalStoriesAllTime: number;
|
|
113
185
|
readonly currentSession: YoloopPersistedSession | null;
|
|
186
|
+
/** P220.11: absent for readable v1.0/v1.1 state. */
|
|
187
|
+
readonly runControl?: YoloopRunControlV1;
|
|
114
188
|
/** P205.08 semantic continuation policy. New state writes v2; absence is legacy v1. */
|
|
115
189
|
readonly continuationPolicy?: 'p137-resume-first-v1' | 'p205-finite-resume-v2';
|
|
116
190
|
/** Opaque, public-safe frozen session/certificate state owned by the root. */
|
|
@@ -144,6 +218,9 @@ export type YoloopStateOperation = {
|
|
|
144
218
|
readonly lock: YoloopPersistedLock;
|
|
145
219
|
} | {
|
|
146
220
|
readonly type: 'release_lock';
|
|
221
|
+
readonly expectedRunId?: string;
|
|
222
|
+
readonly expectedSessionId?: string;
|
|
223
|
+
readonly expectedLeaseId?: string;
|
|
147
224
|
} | {
|
|
148
225
|
readonly type: 'write_state';
|
|
149
226
|
readonly state: YoloopPersistedCounters;
|
|
@@ -169,6 +246,15 @@ export type YoloopStateOperation = {
|
|
|
169
246
|
readonly type: 'switch_continuation_policy';
|
|
170
247
|
readonly policy: 'p137-resume-first-v1' | 'p205-finite-resume-v2';
|
|
171
248
|
readonly session: Readonly<Record<string, unknown>>;
|
|
249
|
+
} | {
|
|
250
|
+
readonly type: 'transition_run';
|
|
251
|
+
readonly runId: string;
|
|
252
|
+
readonly operationId: string;
|
|
253
|
+
readonly idempotencyKey: string;
|
|
254
|
+
readonly expectedRevision: number;
|
|
255
|
+
readonly expectedSnapshotHash: string | null;
|
|
256
|
+
readonly payloadHash: string;
|
|
257
|
+
readonly next: YoloopRunControlV1;
|
|
172
258
|
} | {
|
|
173
259
|
readonly type: 'recovery_attempted';
|
|
174
260
|
readonly attempt: YoloopRecoveryAttemptRef;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Cross-platform: Unix chmod + Windows icacls via existing
|
|
23
23
|
* setSecureFilePermissions helper from secure-config.ts.
|
|
24
24
|
*/
|
|
25
|
-
import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopStateUpdate } from './shared-yoloop-types.js';
|
|
25
|
+
import type { YoloopPersistedCounters, YoloopPersistedState, YoloopRecoveryAttemptRef, YoloopRunControlV1, YoloopRunTransitionReceiptV1, YoloopStateOperation, YoloopStateUpdate } from './shared-yoloop-types.js';
|
|
26
26
|
/** Minimal Pino-compatible logger subset. */
|
|
27
27
|
export interface ClientYoloopLogger {
|
|
28
28
|
warn(payload: object, msg?: string): void;
|
|
@@ -37,6 +37,7 @@ export declare class YoloopClientStateStore {
|
|
|
37
37
|
private readonly stateDir;
|
|
38
38
|
private readonly statePath;
|
|
39
39
|
private readonly lockPath;
|
|
40
|
+
private readonly mutationLockPath;
|
|
40
41
|
private readonly logger;
|
|
41
42
|
constructor(projectRoot: string, logger?: ClientYoloopLogger);
|
|
42
43
|
/**
|
|
@@ -55,6 +56,18 @@ export declare class YoloopClientStateStore {
|
|
|
55
56
|
* single atomic filesystem mutation. Next invoke's readAll() reconciles.
|
|
56
57
|
*/
|
|
57
58
|
applyUpdate(update: YoloopStateUpdate, nowIso?: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Local typed lifecycle-control API. Server transitions and local
|
|
61
|
+
* pause/resume/cancel requests share this serialized CAS boundary.
|
|
62
|
+
*/
|
|
63
|
+
requestRunTransition(operation: Extract<YoloopStateOperation, {
|
|
64
|
+
readonly type: 'transition_run';
|
|
65
|
+
}>, nowIso?: string): Promise<YoloopRunTransitionReceiptV1>;
|
|
66
|
+
/** Persist a cooperative control intent before the next orchestration effect. */
|
|
67
|
+
requestLifecycleAction(action: 'pause' | 'resume' | 'cancel', options?: {
|
|
68
|
+
readonly operationId?: string;
|
|
69
|
+
readonly idempotencyKey?: string;
|
|
70
|
+
}, nowIso?: string): Promise<YoloopRunTransitionReceiptV1>;
|
|
58
71
|
private applyOperation;
|
|
59
72
|
private reserveContinuationAuthority;
|
|
60
73
|
private acknowledgeContinuationAuthority;
|
|
@@ -83,13 +96,13 @@ export declare class YoloopClientStateStore {
|
|
|
83
96
|
* is the authoritative writer of pid/hostname; server can't know them).
|
|
84
97
|
*/
|
|
85
98
|
private acquireLock;
|
|
86
|
-
/** Release the lock.
|
|
99
|
+
/** Release the lock. V1.2 active runs require compare-bound ownership. */
|
|
87
100
|
private releaseLock;
|
|
88
101
|
/** Read lock file. Returns null on ENOENT / parse error. */
|
|
89
102
|
private readLock;
|
|
90
103
|
/** Read state file; returns defaults on any I/O or parse error. */
|
|
91
104
|
private readState;
|
|
92
|
-
/**
|
|
105
|
+
/** Durable atomic state write: same-filesystem temp + fsync + rename + dir fsync. */
|
|
93
106
|
private writeState;
|
|
94
107
|
/**
|
|
95
108
|
* Advance to next epic: read-modify-write on currentSession.currentEpicIndex.
|
|
@@ -98,6 +111,9 @@ export declare class YoloopClientStateStore {
|
|
|
98
111
|
*/
|
|
99
112
|
private advanceEpic;
|
|
100
113
|
private ensureDir;
|
|
114
|
+
private syncStateDirectory;
|
|
115
|
+
private withMutationLock;
|
|
116
|
+
private transitionRun;
|
|
101
117
|
/**
|
|
102
118
|
* Fire-and-forget chmod/ACL. Uses sync helpers from secure-config.ts
|
|
103
119
|
* which are fail-open by contract (never throw).
|
|
@@ -120,6 +136,14 @@ export declare class YoloopClientStateStore {
|
|
|
120
136
|
/** Remove `yoloop-state.json.tmp.*` orphans from crashed writes. */
|
|
121
137
|
private cleanupOrphanTempFiles;
|
|
122
138
|
}
|
|
139
|
+
export declare function buildYoloopRunTransitionPayloadHash(input: {
|
|
140
|
+
readonly runId: string;
|
|
141
|
+
readonly operationId: string;
|
|
142
|
+
readonly idempotencyKey: string;
|
|
143
|
+
readonly expectedRevision: number;
|
|
144
|
+
readonly expectedSnapshotHash: string | null;
|
|
145
|
+
readonly next: YoloopRunControlV1;
|
|
146
|
+
}): string;
|
|
123
147
|
/**
|
|
124
148
|
* Enforce FIFO bound on `recovery_history[]`. Returns a new array of length
|
|
125
149
|
* <= `RECOVERY_HISTORY_CAP`, preserving the most recent entries (drops from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{promises as c}from"node:fs";import{createHash as k}from"node:crypto";import{join as y,dirname as S}from"node:path";import{hostname as _}from"node:os";import{setSecureFilePermissions as E,setSecureDirPermissions as P}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as g}from"./shared-yoloop-types.js";const T=".neocortex",v="yoloop-state.json",R="yoloop.lock",C=360*60*1e3,I={warn:()=>{},info:()=>{},error:()=>{}};function f(i){const e=i.slice(0,10);return{version:"1.1",lastResetAt:i,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 U(i){return{lock:null,state:f(i)}}class q{stateDir;statePath;lockPath;logger;constructor(e,t=I){this.stateDir=y(e,T),this.statePath=y(this.stateDir,v),this.lockPath=y(this.stateDir,R),this.logger=t}async readAll(e){const t=e??new Date().toISOString();await this.cleanupOrphanTempFiles();const[o,r]=await Promise.all([this.readLock(),this.readState(t)]);return{lock:await this.prepareLockForSnapshot(o,t),state:r}}async applyUpdate(e,t){const o=t??new Date().toISOString();await this.ensureDir();for(const r of e.operations)await this.applyOperation(r,o)}async applyOperation(e,t){switch(e.type){case"acquire_lock":await this.acquireLock(e.lock,t);return;case"release_lock":await this.releaseLock();return;case"write_state":await this.writeState(e.state);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"recovery_attempted":await this.appendRecoveryAttempt(e.attempt,t);return;case"recovery_resolved":await this.resolveRecoveryAttempt(e.attempt_id,e.outcome,t);return;default:{const o=e;this.logger.warn({event:"yoloop_unknown_op",op:o},"unknown yoloop operation; skipping");return}}}async reserveContinuationAuthority(e,t){const o=await this.readState(t),r=o.pendingContinuationAuthorities??[],n=typeof e.idempotencyKey=="string"?e.idempotencyKey:"";if(!n)throw new Error("YOLOOP_CONTINUATION_AUTHORITY_INVALID");const s=r.find(a=>a.idempotencyKey===n);if(s){if(u(s)===u(e))return;throw new Error("YOLOOP_CONTINUATION_AUTHORITY_CONFLICT")}if(r.length>=64)throw new Error("YOLOOP_PENDING_AUTHORITY_CAPACITY_EXCEEDED");await this.writeState({...o,pendingContinuationAuthorities:[...r,e]})}async acknowledgeContinuationAuthority(e,t,o){const r=await this.readState(o),n=r.pendingContinuationAuthorities??[],s=n.findIndex(p=>p.idempotencyKey===e);if(s<0)throw new Error("YOLOOP_CONTINUATION_RECEIPT_ORPHAN");const a=n[s];if(t.idempotencyKey!==e||t.sessionId!==a.sessionId||JSON.stringify(t.storyIds)!==JSON.stringify(a.storyIds))throw new Error("YOLOOP_CONTINUATION_RECEIPT_MISMATCH");const l=n.filter((p,h)=>h!==s),d=r.continuationReceipts??[];await this.writeState({...r,pendingContinuationAuthorities:l,continuationReceipts:[...d.slice(-63),t]})}async repairContinuationAuthority(e,t,o,r){const n=await this.readState(r),s=n.pendingContinuationAuthorities??[],a=n.continuationRecoveryReceipts??[],l=typeof o.idempotencyKey=="string"?o.idempotencyKey:"",d=a.find(O=>O.idempotencyKey===l);if(d){if(u(d)===u(o))return;throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_CONFLICT")}if(s.length!==1||w("p208-legacy-authority-before-v1",s[0])!==e)throw new Error("YOLOOP_CONTINUATION_RECOVERY_BEFORE_MISMATCH");const p=w("p208-legacy-authority-after-v1",t);if(o.beforeHash!==e||o.afterHash!==p)throw new Error("YOLOOP_CONTINUATION_RECOVERY_HASH_MISMATCH");const h={...o};if(delete h.idempotencyKey,!l||w("p208-legacy-authority-recovery-receipt-v1",h)!==l)throw new Error("YOLOOP_CONTINUATION_RECOVERY_RECEIPT_INVALID");await this.writeState({...n,pendingContinuationAuthorities:[t],continuationRecoveryReceipts:[...a.slice(-63),o]})}async switchContinuationPolicy(e,t,o){const r=await this.readState(o);if(r.currentSession||(r.pendingContinuationAuthorities?.length??0)>0)throw new Error("YOLOOP_POLICY_SWITCH_ACTIVE");try{await c.copyFile(this.statePath,`${this.statePath}.policy-backup`)}catch(n){if(n.code!=="ENOENT")throw n}await this.writeState({...r,continuationPolicy:e,continuationSession:t})}async appendRecoveryAttempt(e,t){const o=await this.readState(t),r=o.recovery_history??[];if(r.some(l=>l.attempted_at===e.attempted_at&&l.story_id===e.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:e.story_id},"duplicate recovery_attempted op; skipping");return}const s=m([...r,e]),a={...o,recovery_history:s};await this.writeState(a),this.logger.info({event:"recovery_attempted_persisted",story_id:e.story_id,history_size:s.length},"recovery attempt persisted")}async resolveRecoveryAttempt(e,t,o){const r=await this.readState(o),n=r.recovery_history??[],s=n.findIndex(p=>p.recovery_step_id_tokenized===e);if(s===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:e},"recovery_resolved op had no matching attempt; skipping");return}const a=n[s];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 l=n.slice();l[s]={...a,outcome:t,finished_at:o};const d={...r,recovery_history:l};await this.writeState(d),this.logger.info({event:"recovery_resolved_persisted",attempt_id:e,outcome:t},"recovery attempt resolved")}async acquireLock(e,t){const o={...e,pid:process.pid,hostname:_(),ownerKind:"client",startedAt:e.startedAt||t,leaseId:e.leaseId??N(e.sessionId,t),leaseState:"active",leaseAcquiredAt:e.leaseAcquiredAt??t,leaseDurationMs:e.leaseDurationMs??C,leaseExpiresAt:e.leaseExpiresAt??e.ttlExpiresAt},r=JSON.stringify(o,null,2);try{const n=await c.open(this.lockPath,"wx");try{await n.writeFile(r,"utf8")}finally{await n.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:o.pid,sessionId:o.sessionId},"yoloop lock acquired");return}catch(n){const s=n;if(s.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:s.message},"failed to acquire yoloop lock"),n;const a=await this.readLock();if(a&&this.isSameClientSessionLock(a,o)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:o.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"),n;const d=await c.open(this.lockPath,"wx");try{await d.writeFile(r,"utf8")}finally{await d.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:o.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(){try{await c.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(e){const t=e;if(t.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:t.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const e=await c.readFile(this.lockPath,"utf8"),t=JSON.parse(e),o=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??(o?[o]:[]),currentEpicIndex:t.currentEpicIndex??0,currentEpicId:o,ttlExpiresAt:t.ttlExpiresAt??"",epicId:t.epicId??o}}catch{return null}}async readState(e){try{const t=await c.readFile(this.statePath,"utf8"),o=JSON.parse(t);return D(o,e)}catch(t){const o=t;return o.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:o.message},"yoloop state file unreadable; using defaults"),f(e)}}async writeState(e){const t=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,o=JSON.stringify(e,null,2);try{await c.writeFile(t,o,"utf8"),this.trySetFilePerms(t),await c.rename(t,this.statePath)}catch(r){const n=r;this.logger.error({event:"yoloop_state_write_error",err:n.message},"failed to write yoloop state");try{await c.unlink(t)}catch{}throw r}}async advanceEpic(e,t,o){const r=await this.readState(o);if(!r.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:t},"advance_epic with no active session; skipping");return}const n={...r,currentSession:{...r.currentSession,currentEpicIndex:e}};await this.writeState(n),this.logger.info({event:"yoloop_epic_advance",nextEpicId:t,nextIndex:e},"yoloop advanced to next epic")}async ensureDir(){try{await c.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}}trySetFilePerms(e){try{E(e)}catch{}}trySetDirPerms(e){try{P(e)}catch{}}async prepareLockForSnapshot(e,t){if(!e)return null;if(e.ownerKind!=="client")return e;const o=A(e,t),r={...e,leaseState:o};if(o==="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!==_())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(A(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!==_())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 c.unlink(this.lockPath)}catch{}}isSameClientSessionLock(e,t){return e.ownerKind==="client"&&e.sessionId===t.sessionId&&e.pid===process.pid&&e.hostname===_()}async cleanupOrphanTempFiles(){try{const e=S(this.statePath),t=await c.readdir(e),o=`${v}.tmp.`,r=t.filter(n=>n.startsWith(o));for(const n of r)try{await c.unlink(y(e,n))}catch{}}catch{}}}function u(i){return i===null||typeof i!="object"?JSON.stringify(i):Array.isArray(i)?`[${i.map(u).join(",")}]`:`{${Object.entries(i).filter(([,e])=>e!==void 0).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${u(t)}`).join(",")}}`}function w(i,e){return k("sha256").update(`${i}\0${u(e)}`).digest("hex")}function N(i,e){return`lease-${L(`${i}:${e}`)}`}function L(i){let e=2166136261;for(let t=0;t<i.length;t+=1)e^=i.charCodeAt(t),e=Math.imul(e,16777619)>>>0;return e.toString(36).padStart(7,"0")}function A(i,e){const t=i.leaseExpiresAt??i.ttlExpiresAt,o=Date.parse(t),r=Date.parse(e);return!Number.isFinite(o)||!Number.isFinite(r)?"unknown":o<=r?"expired":"active"}function D(i,e){const t=f(e);return{...i,version:"1.1",lastResetAt:i.lastResetAt??t.lastResetAt,storiesRunToday:i.storiesRunToday??0,iterationsToday:i.iterationsToday??0,iterationsTodayDate:i.iterationsTodayDate??t.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,...i.continuationPolicy!==void 0?{continuationPolicy:i.continuationPolicy}:{},...i.continuationSession!==void 0?{continuationSession:i.continuationSession}:{},...i.pendingContinuationAuthorities!==void 0?{pendingContinuationAuthorities:i.pendingContinuationAuthorities}:{},...i.continuationReceipts!==void 0?{continuationReceipts:i.continuationReceipts}:{},...i.continuationRecoveryReceipts!==void 0?{continuationRecoveryReceipts:i.continuationRecoveryReceipts}:{},recovery_history:Array.isArray(i.recovery_history)?m(i.recovery_history):[]}}function m(i){return i.length<=g?i:i.slice(i.length-g)}export{q as YoloopClientStateStore,m as capRecoveryHistory,f as createDefaultCounters,U as createDefaultState};
|
|
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};
|