@ornexus/neocortex 4.60.18 → 4.60.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/sbom.cdx.json +6504 -6410
  2. package/docs/install/installer-diagnostics.md +52 -0
  3. package/install.ps1 +1 -1
  4. package/install.sh +1 -1
  5. package/package.json +27 -4
  6. package/packages/client/dist/agent/refresh-stubs.d.ts +7 -0
  7. package/packages/client/dist/agent/refresh-stubs.js +2 -2
  8. package/packages/client/dist/cache/encrypted-cache.d.ts +3 -0
  9. package/packages/client/dist/cache/encrypted-cache.js +1 -1
  10. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -2
  11. package/packages/client/dist/cli.js +3 -3
  12. package/packages/client/dist/commands/activate.js +7 -7
  13. package/packages/client/dist/commands/config.js +1 -1
  14. package/packages/client/dist/commands/invoke.d.ts +64 -0
  15. package/packages/client/dist/commands/invoke.js +54 -47
  16. package/packages/client/dist/config/secure-config.d.ts +9 -3
  17. package/packages/client/dist/config/secure-config.js +8 -8
  18. package/packages/client/dist/config.d.ts +1 -1
  19. package/packages/client/dist/config.js +1 -1
  20. package/packages/client/dist/continuity/continuity-client-state-store.d.ts +1 -0
  21. package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1
  22. package/packages/client/dist/continuity/invoke-hooks.d.ts +2 -1
  23. package/packages/client/dist/continuity/invoke-hooks.js +1 -1
  24. package/packages/client/dist/continuity/sqlite-store.d.ts +21 -0
  25. package/packages/client/dist/continuity/sqlite-store.js +49 -49
  26. package/packages/client/dist/effects/invoke-effect-gateways.d.ts +22 -0
  27. package/packages/client/dist/effects/invoke-effect-gateways.js +1 -0
  28. package/packages/client/dist/errors/frontmatter-diagnostics.d.ts +20 -0
  29. package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -0
  30. package/packages/client/dist/generated/command-registry.generated.d.ts +2836 -0
  31. package/packages/client/dist/generated/command-registry.generated.js +1 -0
  32. package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +2 -0
  33. package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -1
  34. package/packages/client/dist/index.d.ts +1 -0
  35. package/packages/client/dist/index.js +1 -1
  36. package/packages/client/dist/license/license-client.d.ts +3 -0
  37. package/packages/client/dist/license/license-client.js +1 -1
  38. package/packages/client/dist/machine/fingerprint.d.ts +3 -1
  39. package/packages/client/dist/machine/fingerprint.js +2 -2
  40. package/packages/client/dist/memory/project-memory-writer.js +19 -15
  41. package/packages/client/dist/{subagent-approval-policy.d.ts → policy/subagent-approval-policy.d.ts} +8 -3
  42. package/packages/client/dist/policy/subagent-approval-policy.js +1 -0
  43. package/packages/client/dist/resolvers/local-resolver.d.ts +3 -0
  44. package/packages/client/dist/resolvers/local-resolver.js +4 -4
  45. package/packages/client/dist/runner/cli.d.ts +1 -0
  46. package/packages/client/dist/runner/cli.js +1 -1
  47. package/packages/client/dist/runner/headless/prepared-story-bundle.d.ts +20 -0
  48. package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -0
  49. package/packages/client/dist/runner/headless/run-yoloop.d.ts +2 -0
  50. package/packages/client/dist/runner/headless/run-yoloop.js +1 -1
  51. package/packages/client/dist/runner/portal-sync-worker.js +1 -1
  52. package/packages/client/dist/runner/scheduler.d.ts +3 -0
  53. package/packages/client/dist/runner/scheduler.js +7 -7
  54. package/packages/client/dist/state/project-state-snapshot.js +1 -1
  55. package/packages/client/dist/tier/tier-aware-client.js +1 -1
  56. package/packages/client/dist/version.d.ts +3 -0
  57. package/packages/client/dist/version.js +1 -0
  58. package/packages/client/dist/yoloop/discovery-hook.js +2 -2
  59. package/packages/client/dist/yoloop/invoke-hooks.js +4 -4
  60. package/packages/client/dist/yoloop/shared-discover-epics.d.ts +9 -3
  61. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
  62. package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +24 -0
  63. package/packages/client/dist/yoloop/target-normalization.d.ts +24 -0
  64. package/packages/client/dist/yoloop/target-normalization.js +1 -0
  65. package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +4 -0
  66. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
  67. package/packages/shared/dist/command-registry-contract.js +382 -0
  68. package/packages/shared/dist/generated/command-registry.generated.js +4216 -0
  69. package/packages/shared/dist/generated/invocation-schema.generated.js +1850 -0
  70. package/packages/shared/dist/invocation/index.d.ts +175 -0
  71. package/packages/shared/dist/invocation/index.js +587 -0
  72. package/packages/shared/dist/nx-invoke-signature.js +233 -0
  73. package/packages/shared/dist/strict-readonly.js +381 -0
  74. package/packages/shared/dist/yoloop/wave-decision-receipt.js +77 -0
  75. package/targets-stubs/antigravity/gemini.md +1 -1
  76. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  77. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  78. package/targets-stubs/claude-code/neocortex-root.md +5 -2
  79. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  80. package/targets-stubs/claude-code/neocortex.md +7 -2
  81. package/targets-stubs/codex/AGENTS.md +4 -1
  82. package/targets-stubs/codex/neocortex.toml +4 -0
  83. package/targets-stubs/cursor/agent.md +2 -2
  84. package/targets-stubs/gemini-cli/agent.md +2 -2
  85. package/targets-stubs/opencode/neocortex-root.md +4 -1
  86. package/targets-stubs/opencode/neocortex.md +3 -0
  87. package/targets-stubs/vscode/neocortex.agent.md +2 -2
  88. package/packages/client/dist/subagent-approval-policy.js +0 -1
@@ -1,2 +1,2 @@
1
- import{existsSync as f,readFileSync as m,renameSync as y,statSync as O,writeFileSync as b}from"node:fs";import{basename as p,dirname as j,resolve as l,isAbsolute as w}from"node:path";import{discoverEpicsFromDir as z,mergeDiscoveredIntoState as g,buildDefaultDiscoveryFs as S}from"./shared-discover-epics.js";const u=/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i;function k(e){const o=e.match(/@(\S+)/);if(o)return o[1];const t=e.replace(u,"").trim();return t?t.split(/\s+/).find(i=>i.length>0&&!i.startsWith("-"))??null:null}function A(e,o){return w(e)?e:l(o,e)}function $(e,o,t){if(o.epics.length===0)return;const s=l(e,".neocortex","state.json");if(f(s))try{const i=m(s,"utf8"),r=JSON.parse(i),n=g(r,o);if(n.added.length===0&&n.storiesAdded===0)return;r.last_updated=new Date().toISOString();const d=`${s}.tmp.${process.pid}.${Date.now()}`;b(d,`${JSON.stringify(r,null,2)}
2
- `,"utf8"),y(d,s)}catch(i){const r=i instanceof Error?i.message:String(i);t?.warn?.(`[neocortex] discovery hook: failed to persist .neocortex/state.json: ${r}. Proceeding with request snapshot only.`)}}function v(e,o){const t=p(o),s=e.epics.filter(r=>r.path===o||r.filename===t),i=new Set(s.map(r=>r.id));return{epics:Object.freeze(s),warnings:Object.freeze([...e.warnings,...s.length===0?[`single-file discovery found no matching epic descriptor for ${t}`]:[]]),skipped:Object.freeze([...e.skipped,...e.epics.filter(r=>!i.has(r.id)).map(r=>`${r.filename}: skipped (not requested file)`)])}}async function D(e){if(!e.featureFlag)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"flag off"};if(typeof e.args!="string"||!u.test(e.args))return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"not a yoloop trigger"};const o=k(e.args);if(!o)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"no path arg"};let t;try{t=A(o,e.projectRoot)}catch(r){const n=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook: path resolution failed: ${n}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"path resolution failed"}}let s=!1,i=!1;try{if(f(t)){const r=O(t);s=r.isDirectory(),i=r.isFile()}}catch{s=!1,i=!1}if(!s&&!i)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"path not found"};try{const r=await S(),n=s?t:j(t),d=z(n,r),c=i?v(d,t):d,a=g(e.stateSnapshot,c);$(e.projectRoot,c,e.logger),(a.added.length>0||a.storiesAdded>0)&&e.logger?.info?.(`[neocortex] discovered ${a.added.length} epic(s), ${a.storiesAdded} story-ies from ${s?t:p(t)}`);for(const h of c.warnings)e.logger?.warn?.(`[neocortex] discovery: ${h}`);return{applied:!0,added:a.added,skipped:a.skipped,storiesAdded:a.storiesAdded}}catch(r){const n=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook failed: ${n}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`error: ${n}`}}}async function R(e){if(!e.featureFlag)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"flag off"};try{return await D(e)}catch(o){const t=o instanceof Error?o.message:String(o);return e.logger?.warn?.(`[neocortex] discovery hook (outer) failed: ${t}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`outer error: ${t}`}}}export{R as maybeRunDiscoveryHook,D as runDiscoveryHook};
1
+ import{existsSync as f,readFileSync as h,renameSync as m,statSync as y,writeFileSync as j}from"node:fs";import{basename as p,dirname as b,resolve as l,isAbsolute as O}from"node:path";import{discoverEpicsFromDir as w,mergeDiscoveredIntoState as g,buildDefaultDiscoveryFs as k}from"./shared-discover-epics.js";import{extractYoloopTargetPath as z,isYoloopInvocation as S}from"./target-normalization.js";function v(e,o){return O(e)?e:l(o,e)}function $(e,o,t){if(o.epics.length===0)return;const s=l(e,".neocortex","state.json");if(f(s))try{const i=h(s,"utf8"),r=JSON.parse(i),n=g(r,o);if(n.added.length===0&&n.storiesAdded===0)return;r.last_updated=new Date().toISOString();const a=`${s}.tmp.${process.pid}.${Date.now()}`;j(a,`${JSON.stringify(r,null,2)}
2
+ `,"utf8"),m(a,s)}catch(i){const r=i instanceof Error?i.message:String(i);t?.warn?.(`[neocortex] discovery hook: failed to persist .neocortex/state.json: ${r}. Proceeding with request snapshot only.`)}}function A(e,o){const t=p(o),s=e.epics.filter(r=>r.path===o||r.filename===t),i=new Set(s.map(r=>r.id));return{epics:Object.freeze(s),warnings:Object.freeze([...e.warnings,...s.length===0?[`single-file discovery found no matching epic descriptor for ${t}`]:[]]),skipped:Object.freeze([...e.skipped,...e.epics.filter(r=>!i.has(r.id)).map(r=>`${r.filename}: skipped (not requested file)`)])}}async function D(e){if(!e.featureFlag)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"flag off"};if(!S(e.args))return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"not a yoloop trigger"};const o=z(e.args,e.projectRoot);if(!o)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"no path arg"};let t;try{t=v(o,e.projectRoot)}catch(r){const n=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook: path resolution failed: ${n}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"path resolution failed"}}let s=!1,i=!1;try{if(f(t)){const r=y(t);s=r.isDirectory(),i=r.isFile()}}catch{s=!1,i=!1}if(!s&&!i)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"path not found"};try{const r=await k(),n=s?t:b(t),a=w(n,r),c=i?A(a,t):a,d=g(e.stateSnapshot,c);$(e.projectRoot,c,e.logger),(d.added.length>0||d.storiesAdded>0)&&e.logger?.info?.(`[neocortex] discovered ${d.added.length} epic(s), ${d.storiesAdded} story-ies from ${s?t:p(t)}`);for(const u of c.warnings)e.logger?.warn?.(`[neocortex] discovery: ${u}`);return{applied:!0,added:d.added,skipped:d.skipped,storiesAdded:d.storiesAdded}}catch(r){const n=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook failed: ${n}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`error: ${n}`}}}async function I(e){if(!e.featureFlag)return{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:"flag off"};try{return await D(e)}catch(o){const t=o instanceof Error?o.message:String(o);return e.logger?.warn?.(`[neocortex] discovery hook (outer) failed: ${t}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`outer error: ${t}`}}}export{I as maybeRunDiscoveryHook,D as runDiscoveryHook};
@@ -1,5 +1,5 @@
1
- import{YoloopClientStateStore as u}from"./yoloop-client-state-store.js";function g(e){return/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i.test(e)}const y=/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i;function h(e){if(typeof e.args!="string"||e.args.length===0)return;const r=e.args.match(y);if(!r)return;const t=r[0];if(e.args.slice(t.length).trim()!=="")return;let a=!1;try{const n=require("node:fs"),i=require("node:path").join(e.projectRoot,"docs","epics");a=n.statSync(i).isDirectory()}catch(n){const s=n instanceof Error?n.message:String(n);e.logger?.warn?.(`[Neocortex] Zero-config *yoloop hook (P121.05): could not stat docs/epics/: ${s}. Proceeding with raw args.`);return}a&&(e.args=`${t} @docs/epics/`,e.logger?.info?.("[Neocortex] Zero-config *yoloop (P121.05): auto-resolved empty args to @docs/epics/"))}async function w(e,r,t){if(g(e))try{return await new u(r).readAll(t)}catch(o){process.stderr.write(`[yoloop] Failed to read local state: ${o.message}. Continuing without state.
2
- `);return}}async function k(e,r,t){if(!e)return;const o=e.yoloopStateUpdate;if(!(!o||!Array.isArray(o.operations)||o.operations.length===0))try{await new u(r).applyUpdate(o,t)}catch(a){const n=a.message??String(a);process.stderr.write(`[yoloop] Failed to persist state: ${n}. Your next *yoloop may repeat or skip an operation. Run *yoloop again to retry.
3
- `)}}async function j(e,r){if(!e||e.stateMutated!==!0)return;const t=e.migratedState;if(!(!t||typeof t!="object"||Array.isArray(t)))try{const o=await import("node:fs/promises"),a=await import("node:path"),n=await import("node:crypto"),s=a.join(r,".neocortex"),i=a.join(s,"state.json"),c=a.join(s,`state.json.backup-${new Date().toISOString().replace(/[:.]/g,"-")}`),p=a.join(s,`state.json.tmp.${n.randomBytes(6).toString("hex")}`);await o.mkdir(s,{recursive:!0});try{await o.copyFile(i,c)}catch(l){if(l?.code!=="ENOENT")throw l}const d=JSON.stringify(t,null,2);await o.writeFile(p,d,"utf8"),await o.rename(p,i),process.stderr.write(`[Neocortex P125.03] state.json auto-migrated to canonical v1 schema by server. Legacy planning.* fields converted to epics{} + stories{}; backup=${a.basename(c)}.
1
+ import{YoloopClientStateStore as d}from"./yoloop-client-state-store.js";import{isYoloopInvocation as g}from"./target-normalization.js";function f(e){return g(e)}const y=/^\s*\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i;function k(e){if(typeof e.args!="string"||e.args.length===0)return;const r=e.args.match(y);if(!r)return;const t=r[0];if(e.args.slice(t.length).trim()!=="")return;let a=!1;try{const n=require("node:fs"),i=require("node:path").join(e.projectRoot,"docs","epics");a=n.statSync(i).isDirectory()}catch(n){const s=n instanceof Error?n.message:String(n);e.logger?.warn?.(`[Neocortex] Zero-config *yoloop hook (P121.05): could not stat docs/epics/: ${s}. Proceeding with raw args.`);return}a&&(e.args=`${t} @docs/epics/`,e.logger?.info?.("[Neocortex] Zero-config *yoloop (P121.05): auto-resolved empty args to @docs/epics/"))}async function j(e,r,t){if(f(e))try{return await new d(r).readAll(t)}catch(o){process.stderr.write(`[yoloop] Failed to read local state: ${o.message}. Continuing without state.
2
+ `);return}}async function v(e,r,t){if(!e)return;const o=e.yoloopStateUpdate;if(!(!o||!Array.isArray(o.operations)||o.operations.length===0))try{await new d(r).applyUpdate(o,t)}catch(a){const n=a.message??String(a);process.stderr.write(`[yoloop] Failed to persist state: ${n}. Your next *yoloop may repeat or skip an operation. Run *yoloop again to retry.
3
+ `)}}async function S(e,r){if(!e||e.stateMutated!==!0)return;const t=e.migratedState;if(!(!t||typeof t!="object"||Array.isArray(t)))try{const o=await import("node:fs/promises"),a=await import("node:path"),n=await import("node:crypto"),s=a.join(r,".neocortex"),i=a.join(s,"state.json"),c=a.join(s,`state.json.backup-${new Date().toISOString().replace(/[:.]/g,"-")}`),p=a.join(s,`state.json.tmp.${n.randomBytes(6).toString("hex")}`);await o.mkdir(s,{recursive:!0});try{await o.copyFile(i,c)}catch(l){if(l?.code!=="ENOENT")throw l}const u=JSON.stringify(t,null,2);await o.writeFile(p,u,"utf8"),await o.rename(p,i),process.stderr.write(`[Neocortex P125.03] state.json auto-migrated to canonical v1 schema by server. Legacy planning.* fields converted to epics{} + stories{}; backup=${a.basename(c)}.
4
4
  `)}catch(o){const a=o.message??String(o);process.stderr.write(`[Neocortex P125.03] Failed to persist auto-migrated state: ${a}. Your next invoke will retry (server runs migrate idempotently). If the warning persists, run \`neocortex invoke "*migrate-state --apply"\` manually.
5
- `)}}async function S(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 t=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook (lazy load) failed: ${t}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`lazy load error: ${t}`}}}async function b(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 t=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] graph retrieval hook (lazy load) failed: ${t}. Proceeding without enrichment.`),{applied:!1,reason:"hook_error",warning:t}}}export{g as isYoloopTrigger,h as maybeAutoResolveYoloopArgs,S as maybeRunDiscoveryHook,b as maybeRunGraphRetrievalHook,j as persistMigratedStateFromResponse,k as persistYoloopFromResponse,w as readYoloopForRequest};
5
+ `)}}async function b(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 t=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] discovery hook (lazy load) failed: ${t}. Proceeding without enrichment.`),{applied:!1,added:Object.freeze([]),skipped:Object.freeze([]),storiesAdded:0,reason:`lazy load error: ${t}`}}}async function R(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 t=r instanceof Error?r.message:String(r);return e.logger?.warn?.(`[neocortex] graph retrieval hook (lazy load) failed: ${t}. Proceeding without enrichment.`),{applied:!1,reason:"hook_error",warning:t}}}export{f as isYoloopTrigger,k as maybeAutoResolveYoloopArgs,b as maybeRunDiscoveryHook,R as maybeRunGraphRetrievalHook,S as persistMigratedStateFromResponse,v as persistYoloopFromResponse,j as readYoloopForRequest};
@@ -46,10 +46,12 @@
46
46
  * Source of the canonical epic ID resolution.
47
47
  *
48
48
  * - `'frontmatter'`: `id:` field in YAML frontmatter (highest priority)
49
+ * - `'legacy-metadata'`: bounded top-of-file Markdown metadata
49
50
  * - `'filename-canonical'`: matched `EPIC_FILE_PATTERNS[0]` or `[1]` (numeric)
50
51
  * - `'filename-slug'`: matched `EPIC_FILE_PATTERNS[2]` (alpha-slug fallback)
51
52
  */
52
- export type EpicIdSource = 'frontmatter' | 'filename-canonical' | 'filename-slug';
53
+ export type EpicIdSource = 'frontmatter' | 'legacy-metadata' | 'filename-canonical' | 'filename-slug';
54
+ export type EpicMetadataSource = 'frontmatter' | 'legacy-metadata' | 'none';
53
55
  /**
54
56
  * Descriptor for a discovered epic file.
55
57
  * Wire-format public (consumed by P120.02 merge fn + admin observability).
@@ -61,10 +63,12 @@ export interface EpicDescriptor {
61
63
  readonly filename: string;
62
64
  /** Path relative to the scan dir. */
63
65
  readonly path: string;
64
- /** Optional title from frontmatter. */
66
+ /** Optional title from YAML frontmatter or bounded legacy metadata. */
65
67
  readonly title?: string;
66
- /** Optional status from frontmatter. */
68
+ /** Optional normalized status from YAML frontmatter or bounded legacy metadata. */
67
69
  readonly status?: string;
70
+ /** Optional declared story count from file metadata. */
71
+ readonly total_stories?: number;
68
72
  /** Optional epic dependencies from frontmatter. */
69
73
  readonly depends_on?: readonly string[];
70
74
  /** Provenance marker -- always 'discovered' from this fn. */
@@ -75,6 +79,8 @@ export interface EpicDescriptor {
75
79
  readonly stories: ReadonlyArray<StoryDescriptor>;
76
80
  /** Internal: how was the canonical ID resolved (debugging + tests). */
77
81
  readonly _idSource: EpicIdSource;
82
+ /** Internal: metadata parser selected for this descriptor. */
83
+ readonly _metadataSource?: EpicMetadataSource;
78
84
  }
79
85
  /**
80
86
  * Descriptor for a discovered story file.
@@ -1 +1 @@
1
- const Q=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"])})}),$=Object.freeze([/^epic-(P?\d+(?:\.\d+)?)\.md$/i,/^EPIC-(\d+)(?:[-_].*)?\.md$/,/^epic-([a-z0-9][a-z0-9_-]*)\.md$/i]),E=Object.freeze([/^(P?\d+\.\d+(?:\.\d+)?)\.story\.md$/i,/^story-(P?\d+\.\d+)\.md$/i,/^EPIC-(\d+)\.STORY-(\d+)\.md$/]),x=Object.freeze(["_archive","_drafts","archive","drafts"]),T=/^---\s*\n([\s\S]*?)\n---/;function S(t,e){const n=new RegExp(`^${e}:\\s*(.+?)\\s*$`,"mi"),s=t.match(n);if(!s)return;let o=s[1].trim();return(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),o||void 0}function w(t){if(typeof t!="string"||t.length===0)return{};const e=t.match(T);if(!e)return{};const n=e[1];return{id:S(n,"id"),title:S(n,"title"),status:S(n,"status"),epic_id:S(n,"epic_id"),depends_on:A(n),files_to_modify:v(n,"files_to_modify")}}function F(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 A(t){return v(t,"depends_on")}function v(t,e){const n=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=t.match(new RegExp(`^${n}:[ \\t]*(.+?)[ \\t]*$`,"mi"))?.[1]?.trim();if(o!==void 0){const g=(o.startsWith("[")&&o.endsWith("]")?o.slice(1,-1).split(","):[o]).map(F).filter(f=>typeof f=="string"&&f.length>0);return g.length>0?Object.freeze(g):void 0}const u=t.split(/\r?\n/),p=[];let l=!1;for(const _ of u){if(!l){new RegExp(`^${n}:\\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 f=F(g[1]);f&&p.push(f)}return p.length>0?Object.freeze(p):void 0}function D(t){return t.startsWith("_")||t.startsWith(".")}function W(t){return x.includes(t)}async function V(){const t=await import("node:fs"),e=await import("node:path");return{isDirectory(n){try{return t.statSync(n).isDirectory()}catch{return!1}},readdir(n){try{return t.readdirSync(n)}catch{return[]}},readFile(n){try{return t.readFileSync(n,"utf8")}catch{return""}},join(...n){try{return e.join(...n)}catch{return n.join("/")}}}}function k(t,e){let n=!1,s=null,o=null;for(let u=0;u<$.length;u++){const p=$[u],l=t.match(p);if(l){n=!0,s=l[1],o=u===2?"filename-slug":"filename-canonical";break}}return n?e.id&&e.id.length>0?{id:e.id,source:"frontmatter"}:{id:s,source:o}:null}function C(t){for(const e of $){const n=t.match(e);if(n)return n[1]}return null}function O(t){const e=t.trim();return/^\d+(?:\.\d+)*$/i.test(e)?`P${e}`.toUpperCase():e.toUpperCase()}function M(t){const e=O(t);return/^P\d+(?:\.\d+)?$/i.test(e)?`epic-${e}.md`:`epic-${t}.md`}function L(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 B(t,e,n){return`frontmatter/filename mismatch: epic '${t}' has frontmatter id '${e}' but filename implies '${n}'. Repair by using generated canonical '${M(e)}' or changing frontmatter id to '${n}'.`}function G(t,e,n){return`frontmatter/filename mismatch: story '${t}' has frontmatter id '${e}' but filename implies '${n}'. Repair by using generated canonical '${L(e)}' or changing frontmatter id to '${n}'.`}function U(t){let e=t.match(E[0]);if(e){const n=e[1],s=n.indexOf(".");return{id:n,epic_id_hint:s>0?n.slice(0,s):null}}if(e=t.match(E[1]),e){const n=e[1],s=n.indexOf(".");return{id:n,epic_id_hint:s>0?n.slice(0,s):null}}if(e=t.match(E[2]),e){const n=e[1],s=e[2];return{id:`${n}.${s}`,epic_id_hint:n}}return null}function Y(t,e){return e.join(t,"..","stories")}function X(t,e){const n=[],s=[],o=[],u=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 p=[...e.readdir(t)].sort(),l=[];for(const i of p){const r=e.join(t,i);if(D(i)){s.push(`${i}: ignored (draft or hidden prefix)`);continue}if(e.isDirectory(r)){if(W(i)){s.push(`${i}: ignored subdirectory`);continue}s.push(`${i}: subdirectory not recursed`);continue}if(!i.endsWith(".md"))continue;if(!$.some(b=>b.test(i))){s.push(`${i}: did not match any EPIC_FILE_PATTERNS`);continue}const d=e.readFile(r),a=w(d),m=C(i);a.id&&P(a.id,m)&&n.push(B(i,a.id,m)),l.push({filename:i,fullPath:r,frontmatter:a})}const _=new Map,g=[];for(const i of l){const r=k(i.filename,i.frontmatter);if(r===null){s.push(`${i.filename}: id resolution returned null`);continue}let c=r.id;const d=c,a=_.get(d)??0;a>0&&(c=`${d}-${a+1}`,n.push(`epic ID collision: '${d}' resolved to '${c}' for ${i.filename}`)),_.set(d,a+1),g.push({candidate:i,finalId:c,idSource:r.source})}const f=new Map,y=Y(t,e);if(e.isDirectory(y)){const i=[...e.readdir(y)].sort();for(const r of i){if(D(r)||!r.endsWith(".md"))continue;const c=e.join(y,r);if(e.isDirectory(c))continue;const d=U(r);if(!d)continue;const a=e.readFile(c),m=w(a);m.id&&P(m.id,d.id)&&n.push(G(r,m.id,d.id));const b=m.epic_id??d.epic_id_hint;if(!b){n.push(`story '${r}' has no epic_id (frontmatter or filename pattern)`);continue}const N=Object.freeze({id:d.id,epic_id:b,filename:r,path:c,...m.title?{title:m.title}:{},...m.status?{status:m.status}:{},...m.depends_on?{depends_on:m.depends_on}:{},...m.files_to_modify?{files_to_modify:m.files_to_modify}:{}});f.has(b)||f.set(b,[]),f.get(b).push(N)}}for(const i of g){const r=f.get(i.finalId)??[],c=Object.freeze({id:i.finalId,filename:i.candidate.filename,path:i.candidate.fullPath,...i.candidate.frontmatter.title?{title:i.candidate.frontmatter.title}:{},...i.candidate.frontmatter.status?{status:i.candidate.frontmatter.status}:{},...i.candidate.frontmatter.depends_on?{depends_on:i.candidate.frontmatter.depends_on}:{},source:"discovered",discovered_at:u,stories:Object.freeze([...r]),e:i.idSource});o.push(c)}const j=new Set(o.map(i=>i.id));for(const[i,r]of f.entries())if(!j.has(i))for(const c of r)n.push(`orphan story '${c.filename}' references unknown epic '${i}'`);return Object.freeze({epics:Object.freeze(o),warnings:Object.freeze(n),skipped:Object.freeze(s)})}function H(t){return z(t)}function z(t){const e=Z(t);return e==="done"||e==="completed"||e==="merged"}function I(t){return z(t)}function Z(t){return typeof t=="string"?t.trim().toLowerCase().replace(/[_\s]+/g,"-"):""}function R(t){return!!t&&typeof t=="object"&&!Array.isArray(t)}function h(t){throw new Error(`epic-aggregate-writer:${t}`)}function q(t){const e=[],n=new Set;for(const s of t)(typeof s!="string"||s.length===0)&&h("epic_aggregate_story_ref_invalid"),n.has(s)&&h("epic_aggregate_story_ref_duplicate"),n.add(s),e.push(s);return e}function J(t,e,n){const s=n[e];return s===void 0&&h("epic_aggregate_story_missing"),R(s)||h("epic_aggregate_story_not_object"),s.id!==e&&h("epic_aggregate_story_id_conflict"),s.epic_id!==t&&h("epic_aggregate_story_epic_conflict"),s}function K(t,e,n,s){const o=e[t];R(o)||h("epic_aggregate_epic_not_object"),o.id!==t&&h("epic_aggregate_epic_id_conflict"),Array.isArray(o.stories)||h("epic_aggregate_story_list_not_array");const u=q(o.stories),p=u.length,l=u.filter(g=>z(J(t,g,n).status)).length,_=p>0&&l===p;I(o.status)&&!_&&p>0&&h("epic_aggregate_terminal_incomplete_conflict"),o.total_stories=p,o.completed_stories=l,!I(o.status)&&_&&(o.status="done",(o.completed_at===void 0||o.completed_at===null)&&(o.completed_at=s))}function ee(t,e,n){const s=t??{};(!s.epics||typeof s.epics!="object")&&(s.epics={}),(!s.stories||typeof s.stories!="object")&&(s.stories={});const o=s.epics,u=s.stories,p=[],l=[],_=new Set;let g=0;const f=n?.forceOverwrite===!0,y=n?.nowIso??new Date().toISOString(),j=new Set;for(const i of e.epics)j.add(i.id);for(const i of e.epics){const r=o[i.id];if(r&&r.source==="create-epic"&&!f){l.push({id:i.id,reason:"create-epic source preserved (defense)"});continue}if(r&&r.source==="discovered"&&!f){const d=r;d.depends_on=i.depends_on??[],d.stories=i.stories.map(a=>a.id),i.title!==void 0&&(d.title=i.title),i.status!==void 0&&!I(d.status)&&(d.status=i.status),_.add(i.id);continue}const c={id:i.id,title:i.title??"",status:i.status??"planning",source:"discovered",discovered_at:y,merged_at:y,stories:i.stories.map(d=>d.id),depends_on:i.depends_on??[]};o[i.id]=c,_.add(i.id),p.push(i.id)}for(const i of e.epics)for(const r of i.stories){const c=u[r.id];if(c&&c.source==="create-epic"&&!f){l.push({id:r.id,reason:"create-epic source preserved (defense, story)"});continue}if(c&&H(c.status)&&!f){const a=c;a.depends_on=r.depends_on??[],a.files_to_modify=r.files_to_modify??[];continue}if(c&&c.source==="discovered"&&!f){const a=c;a.depends_on=r.depends_on??[],a.files_to_modify=r.files_to_modify??[];continue}const d={id:r.id,epic_id:r.epic_id,title:r.title??"",status:r.status??"backlog",source:"discovered",discovered_at:y,depends_on:r.depends_on??[],files_to_modify:r.files_to_modify??[]};u[r.id]=d,g++}for(const i of _)K(i,o,u,y);return{state:s,added:Object.freeze([...p]),skipped:Object.freeze([...l]),storiesAdded:g}}export{$ as EPIC_FILE_PATTERNS,Q as GENERATED_NAMING_CONTRACT,E as STORY_FILE_PATTERNS,V as buildDefaultDiscoveryFs,X as discoverEpicsFromDir,k as getEpicIdFromDescriptor,ee as mergeDiscoveredIntoState};
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$/]),x=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 a=n[1].trim();return(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),a||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,Y=4096,G=/^\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(a=>/^#\s+\S/.test(a));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 a=[...new Set(t.map(W).filter(Boolean))];if(a.length>1){n.push(`legacy metadata ambiguous for '${e}' in '${i}': conflicting values ignored`);return}return a[0]}function X(t,e){const i=[],n=[],a=t.slice(0,Y).split(/\r?\n/,C);for(const d of a){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 u=d.match(G);if(!u)continue;const m=U(u[1]);if(!m)continue;const h=o.get(m)??[];h.push(u[2]),o.set(m,h)}const f=v(o.get("id")??[],"id",e,i),l=v(o.get("title")??[],"title",e,i),g=v(o.get("status")??[],"status",e,i),_=v(o.get("total_stories")??[],"total_stories",e,i),p=g===void 0?void 0:H(g);g!==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=f&&/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(f)?f:void 0;f!==void 0&&b===void 0&&i.push(`legacy metadata id invalid in '${e}': value ignored`);const r=Z(n),s=l??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 f=n[1].trim();if(f.startsWith('"')&&!f.endsWith('"')||f.startsWith("'")&&!f.endsWith("'"))return!0;continue}if(/^\s+/.test(i))return!0;const a=i.match(/^[A-Za-z0-9_-]+:\s*(.*?)\s*$/);if(!a)return!0;const o=a[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),a=t.match(A);if(!a)return Object.freeze([`metadata-frontmatter reason=frontmatter-malformed file='${n}': repair the bounded leading YAML envelope; source content omitted`]);const o=a[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,"\\$&"),a=t.match(new RegExp(`^${i}:[ \\t]*(.+?)[ \\t]*$`,"mi"))?.[1]?.trim();if(a!==void 0){const _=(a.startsWith("[")&&a.endsWith("]")?a.slice(1,-1).split(","):[a]).map(L).filter(p=>typeof p=="string"&&p.length>0);return _.length>0?Object.freeze(_):void 0}const o=t.split(/\r?\n/),f=[];let l=!1;for(const g of o){if(!l){new RegExp(`^${i}:\\s*$`,"i").test(g.trim())&&(l=!0);continue}if(/^[A-Za-z0-9_-]+:\s*/.test(g))break;const _=g.match(/^\s*-\s*(.+?)\s*$/);if(!_)continue;const p=L(_[1]);p&&f.push(p)}return f.length>0?Object.freeze(f):void 0}function N(t){return t.startsWith("_")||t.startsWith(".")}function ee(t){return x.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,a=null;for(let o=0;o<E.length;o++){const f=E[o],l=t.match(f);if(l){i=!0,n=l[1],a=o===2?"filename-slug":"filename-canonical";break}}return i?e.id&&e.id.length>0?{id:e.id,source:"frontmatter"}:{id:n,source:a}: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 ae(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 ce(t,e){return e.join(t,"..","stories")}function he(t,e){const i=[],n=[],a=[],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 r of f){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),u=F(d,r);i.push(...u.warnings);const m=u.metadata,h=ie(r);m.id&&P(m.id,h)&&i.push(u.source==="frontmatter"?re(r,m.id,h):`legacy metadata/filename mismatch: epic '${r}' id does not match its filename`),l.push({filename:r,fullPath:s,metadata:m,metadataSource:u.source})}const g=new Map,_=[];for(const r of l){const s=te(r.filename,r.metadata);if(s===null){n.push(`${r.filename}: id resolution returned null`);continue}let c=s.id;const d=c,u=g.get(d)??0;u>0&&(c=`${d}-${u+1}`,i.push(`epic ID collision: '${d}' resolved to '${c}' for ${r.filename}`)),g.set(d,u+1),_.push({candidate:r,finalId:c,idSource:r.metadataSource==="legacy-metadata"&&r.metadata.id?"legacy-metadata":s.source})}const p=new Map,y=ce(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 c=e.join(y,s);if(e.isDirectory(c))continue;const d=ae(s);if(!d)continue;const u=e.readFile(c),m=F(u,s);i.push(...m.warnings);const h=m.metadata;h.id&&P(h.id,d.id)&&i.push(m.source==="frontmatter"?oe(s,h.id,d.id):`legacy metadata/filename mismatch: story '${s}' id does not match its filename`);const S=h.epic_id??d.epic_id_hint;if(!S){i.push(`story '${s}' has no epic_id (frontmatter or filename pattern)`);continue}const k=Object.freeze({id:d.id,epic_id:S,filename:s,path:c,...h.title?{title:h.title}:{},...h.status?{status:h.status}:{},...h.depends_on?{depends_on:h.depends_on}:{},...h.files_to_modify?{files_to_modify:h.files_to_modify}:{}});p.has(S)||p.set(S,[]),p.get(S).push(k)}}for(const r of _){const s=p.get(r.finalId)??[],c=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});a.push(c)}const b=new Set(a.map(r=>r.id));for(const[r,s]of p.entries())if(!b.has(r))for(const c of s)i.push(`orphan story '${c.filename}' references unknown epic '${r}'`);return Object.freeze({epics:Object.freeze(a),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,a){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 f=fe(o.stories),l=f.length,g=f.filter(s=>I(le(t,s,i).status)).length,_=a?.total_stories,p=a?.e==="legacy-metadata"&&z(o.status),y=p?_??l:l>0?l:_??0,b=p||l===0&&y>0&&z(o.status)?y:g,r=l>0&&g===l;z(o.status)&&!p&&!r&&l>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 a=n.epics,o=n.stories,f=[],l=[],g=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 c=a[s.id];if(c&&c.source==="create-epic"&&!y){l.push({id:s.id,reason:"create-epic source preserved (defense)"});continue}if(c&&c.source==="discovered"&&!y){const u=c;u.depends_on=s.depends_on??[],u.stories=s.stories.map(m=>m.id),s.title!==void 0&&(u.title=s.title),s.status!==void 0&&!z(u.status)&&(u.status=s.status),g.add(s.id);continue}const d={id:s.id,title:s.title??"",status:s.status??"planning",source:"discovered",discovered_at:b,merged_at:b,stories:s.stories.map(u=>u.id),depends_on:s.depends_on??[]};a[s.id]=d,g.add(s.id),f.push(s.id)}for(const s of e.epics)for(const c of s.stories){const d=o[c.id];if(d&&d.source==="create-epic"&&!y){l.push({id:c.id,reason:"create-epic source preserved (defense, story)"});continue}if(d&&de(d.status)&&!y){const m=d;m.depends_on=c.depends_on??[],m.files_to_modify=c.files_to_modify??[];continue}if(d&&d.source==="discovered"&&!y){const m=d;m.depends_on=c.depends_on??[],m.files_to_modify=c.files_to_modify??[];continue}const u={id:c.id,epic_id:c.epic_id,title:c.title??"",status:c.status??"backlog",source:"discovered",discovered_at:b,depends_on:c.depends_on??[],files_to_modify:c.files_to_modify??[]};o[c.id]=u,p++}for(const s of g)pe(s,a,o,b,_.get(s));return{state:n,added:Object.freeze([...f]),skipped:Object.freeze([...l]),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};
@@ -111,6 +111,13 @@ export interface YoloopPersistedCounters {
111
111
  readonly totalRunsAllTime: number;
112
112
  readonly totalStoriesAllTime: number;
113
113
  readonly currentSession: YoloopPersistedSession | null;
114
+ /** P205.08 semantic continuation policy. New state writes v2; absence is legacy v1. */
115
+ readonly continuationPolicy?: 'p137-resume-first-v1' | 'p205-finite-resume-v2';
116
+ /** Opaque, public-safe frozen session/certificate state owned by the root. */
117
+ readonly continuationSession?: Readonly<Record<string, unknown>>;
118
+ readonly pendingContinuationAuthorities?: readonly Readonly<Record<string, unknown>>[];
119
+ readonly continuationReceipts?: readonly Readonly<Record<string, unknown>>[];
120
+ readonly continuationRecoveryReceipts?: readonly Readonly<Record<string, unknown>>[];
114
121
  /**
115
122
  * P104.06: FIFO bounded audit trail of self-healing recovery attempts.
116
123
  * Optional for backward compat (clients pre-P104.06 omit the field; readers
@@ -125,6 +132,7 @@ export interface YoloopPersistedCounters {
125
132
  export interface YoloopPersistedState {
126
133
  readonly lock: YoloopPersistedLock | null;
127
134
  readonly state: YoloopPersistedCounters;
135
+ readonly continuationRecoveryEvidence?: Readonly<Record<string, unknown>>;
128
136
  }
129
137
  /**
130
138
  * Typed mutation operations the server returns for the client to apply.
@@ -145,6 +153,22 @@ export type YoloopStateOperation = {
145
153
  readonly nextEpicId: string;
146
154
  } | {
147
155
  readonly type: 'no_op';
156
+ } | {
157
+ readonly type: 'reserve_continuation_authority';
158
+ readonly authority: Readonly<Record<string, unknown>>;
159
+ } | {
160
+ readonly type: 'acknowledge_continuation_authority';
161
+ readonly idempotencyKey: string;
162
+ readonly receipt: Readonly<Record<string, unknown>>;
163
+ } | {
164
+ readonly type: 'repair_continuation_authority';
165
+ readonly beforeHash: string;
166
+ readonly authority: Readonly<Record<string, unknown>>;
167
+ readonly receipt: Readonly<Record<string, unknown>>;
168
+ } | {
169
+ readonly type: 'switch_continuation_policy';
170
+ readonly policy: 'p137-resume-first-v1' | 'p205-finite-resume-v2';
171
+ readonly session: Readonly<Record<string, unknown>>;
148
172
  } | {
149
173
  readonly type: 'recovery_attempted';
150
174
  readonly attempt: YoloopRecoveryAttemptRef;
@@ -0,0 +1,24 @@
1
+ export type YoloopTargetNormalizationReasonCode = 'yoloop-target-unchanged' | 'agent-selector-removed' | 'markdown-link-target-normalized' | 'basename-target-canonicalized' | 'markdown-link-target-malformed' | 'markdown-link-target-ambiguous' | 'markdown-link-target-external' | 'markdown-link-target-unsafe-path' | 'raw-target-malformed' | 'raw-target-external' | 'raw-target-unsafe-path';
2
+ export interface ParsedYoloopInvocation {
3
+ readonly triggerToken: string;
4
+ readonly remainder: string;
5
+ readonly agentSelectorRemoved: boolean;
6
+ }
7
+ export type YoloopTargetNormalizationResult = {
8
+ readonly ok: true;
9
+ readonly args: string;
10
+ readonly changed: boolean;
11
+ readonly reasonCodes: readonly YoloopTargetNormalizationReasonCode[];
12
+ } | {
13
+ readonly ok: false;
14
+ readonly errorCode: 'INVALID_YOLOOP_TARGET';
15
+ readonly reasonCode: YoloopTargetNormalizationReasonCode;
16
+ readonly message: string;
17
+ };
18
+ export interface NormalizeYoloopTargetOptions {
19
+ readonly projectRoot?: string;
20
+ }
21
+ export declare function parseYoloopInvocation(args: unknown): ParsedYoloopInvocation | null;
22
+ export declare function isYoloopInvocation(args: unknown): boolean;
23
+ export declare function normalizeYoloopTargetArgs(args: unknown, options?: NormalizeYoloopTargetOptions): YoloopTargetNormalizationResult;
24
+ export declare function extractYoloopTargetPath(args: unknown, projectRoot?: string): string | null;
@@ -0,0 +1 @@
1
+ import{existsSync as h}from"node:fs";import{resolve as p}from"node:path";import{formatFrontmatterDiagnostic as A}from"../errors/frontmatter-diagnostics.js";const O=/^\*?(yoloop|auto-yolo|epic-runner|story-runner|loop-yolo|auto-epic)\b/i,T=/^@neocortex\b\s*/i,_=/\[[^\]\r\n]*\]\(([^)\r\n]+)\)/g,E=/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\/?$/,R=/^epic-[A-Za-z0-9._-]+\.md$/i;function g(t){if(typeof t!="string")return null;let e=t.trimStart(),n=!1;const r=e.match(T);r&&(e=e.slice(r[0].length).trimStart(),n=!0);const o=e.match(O);return o?Object.freeze({triggerToken:o[0],remainder:e.slice(o[0].length).trim(),agentSelectorRemoved:n}):null}function v(t){return g(t)!==null}function u(t){return Object.freeze({ok:!1,errorCode:"INVALID_YOLOOP_TARGET",reasonCode:t,message:`INVALID_YOLOOP_TARGET: ${A(t)}`})}function k(t){let e=t.trim().replace(/^@/,"").replace(/^\.\//,"");if(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(e)||e.startsWith("//"))return{ok:!1,failure:"external"};if(e.startsWith("/")||e.startsWith("~")||/^[A-Za-z]:[\\/]/.test(e)||e.includes("\\")||e.includes("\0"))return{ok:!1,failure:"unsafe-path"};const n=e.split("/").filter(Boolean);return!e||!E.test(e)||n.some(r=>r==="."||r==="..")?{ok:!1,failure:"unsafe-path"}:!e.endsWith("/")&&!e.toLowerCase().endsWith(".md")?{ok:!1,failure:"malformed"}:(e=e.replace(/^\.\//,""),{ok:!0,target:e})}function $(t){return`markdown-link-target-${t}`}function b(t){return`raw-target-${t}`}function w(t,e){return t.startsWith("@")||e.includes("/")||e.includes("\\")||e.startsWith(".")||e.startsWith("~")||e.toLowerCase().endsWith(".md")||/^[A-Za-z][A-Za-z0-9+.-]*:/.test(e)}function z(t,e){if(!e||!R.test(t)||h(p(e,t)))return t;const n=`docs/epics/${t}`;return h(p(e,n))?n:t}function f(t){return Object.freeze([...t])}function W(t,e={}){const n=g(t);if(!n)return Object.freeze({ok:!0,args:typeof t=="string"?t:"",changed:!1,reasonCodes:f(["yoloop-target-unchanged"])});const r=[];n.agentSelectorRemoved&&r.push("agent-selector-removed");const o=n.remainder;if(!o){const a=n.triggerToken;return Object.freeze({ok:!0,args:a,changed:a!==t,reasonCodes:f(r.length>0?r:["yoloop-target-unchanged"])})}const i=[...o.matchAll(_)],d=o.startsWith("[")||o.includes("](");if(i.length>1)return u("markdown-link-target-ambiguous");if(d&&i.length===0)return u("markdown-link-target-malformed");if(i.length===1){const a=i[0];if(a[0]!==o)return u("markdown-link-target-ambiguous");const s=k(a[1]);if(!s.ok)return u($(s.failure));const l=z(s.target,e.projectRoot);return r.push("markdown-link-target-normalized"),l!==s.target&&r.push("basename-target-canonicalized"),Object.freeze({ok:!0,args:`${n.triggerToken} @${l}`,changed:!0,reasonCodes:f(r)})}const c=o.match(/^@?([^\s]+)$/)?.[1];if(c){let a=c;if(w(o,c)){const l=k(c);if(!l.ok)return u(b(l.failure));a=l.target}const s=z(a,e.projectRoot);if(s!==c)return r.push("basename-target-canonicalized"),Object.freeze({ok:!0,args:`${n.triggerToken} @${s}`,changed:!0,reasonCodes:f(r)})}const m=`${n.triggerToken}${o?` ${o}`:""}`;return Object.freeze({ok:!0,args:m,changed:m!==t,reasonCodes:f(r.length>0?r:["yoloop-target-unchanged"])})}function P(t,e){const n=W(t,{projectRoot:e});if(!n.ok)return null;const r=g(n.args);if(!r?.remainder)return null;const o=r.remainder.match(/(?:^|\s)@([^\s]+)/);if(o)return o[1];const i=r.remainder.split(/\s+/).find(d=>d.length>0&&!d.startsWith("-"));return i?i.replace(/^(?:"|')|(?:"|')$/g,""):null}export{P as extractYoloopTargetPath,v as isYoloopInvocation,W as normalizeYoloopTargetArgs,g as parseYoloopInvocation};
@@ -56,6 +56,10 @@ export declare class YoloopClientStateStore {
56
56
  */
57
57
  applyUpdate(update: YoloopStateUpdate, nowIso?: string): Promise<void>;
58
58
  private applyOperation;
59
+ private reserveContinuationAuthority;
60
+ private acknowledgeContinuationAuthority;
61
+ private repairContinuationAuthority;
62
+ private switchContinuationPolicy;
59
63
  /**
60
64
  * Append a new recovery attempt to `state.recovery_history[]`. FIFO cap
61
65
  * via `capRecoveryHistory`. Idempotent against retry storms: duplicate
@@ -1 +1 @@
1
- import{promises as a}from"node:fs";import{join as p,dirname as g}from"node:path";import{hostname as h}from"node:os";import{setSecureFilePermissions as k,setSecureDirPermissions as m}from"../config/secure-config.js";import{RECOVERY_HISTORY_CAP as y}from"./shared-yoloop-types.js";const S=".neocortex",_="yoloop-state.json",A="yoloop.lock",E=360*60*1e3,D={warn:()=>{},info:()=>{},error:()=>{}};function u(i){const t=i.slice(0,10);return{version:"1.1",lastResetAt:i,storiesRunToday:0,iterationsToday:0,iterationsTodayDate:t,tokensUsedToday:0,lastRunAt:null,coolDownUntil:null,totalRunsAllTime:0,totalStoriesAllTime:0,currentSession:null}}function q(i){return{lock:null,state:u(i)}}class b{stateDir;statePath;lockPath;logger;constructor(t,e=D){this.stateDir=p(t,S),this.statePath=p(this.stateDir,_),this.lockPath=p(this.stateDir,A),this.logger=e}async readAll(t){const e=t??new Date().toISOString();await this.cleanupOrphanTempFiles();const[r,o]=await Promise.all([this.readLock(),this.readState(e)]);return{lock:await this.prepareLockForSnapshot(r,e),state:o}}async applyUpdate(t,e){const r=e??new Date().toISOString();await this.ensureDir();for(const o of t.operations)await this.applyOperation(o,r)}async applyOperation(t,e){switch(t.type){case"acquire_lock":await this.acquireLock(t.lock,e);return;case"release_lock":await this.releaseLock();return;case"write_state":await this.writeState(t.state);return;case"advance_epic":await this.advanceEpic(t.nextEpicIndex,t.nextEpicId,e);return;case"no_op":this.logger.info({event:"yoloop_noop"},"yoloop server sent no-op");return;case"recovery_attempted":await this.appendRecoveryAttempt(t.attempt,e);return;case"recovery_resolved":await this.resolveRecoveryAttempt(t.attempt_id,t.outcome,e);return;default:{const r=t;this.logger.warn({event:"yoloop_unknown_op",op:r},"unknown yoloop operation; skipping");return}}}async appendRecoveryAttempt(t,e){const r=await this.readState(e),o=r.recovery_history??[];if(o.some(l=>l.attempted_at===t.attempted_at&&l.story_id===t.story_id)){this.logger.warn({event:"recovery_attempted_duplicate",story_id:t.story_id},"duplicate recovery_attempted op; skipping");return}const n=w([...o,t]),c={...r,recovery_history:n};await this.writeState(c),this.logger.info({event:"recovery_attempted_persisted",story_id:t.story_id,history_size:n.length},"recovery attempt persisted")}async resolveRecoveryAttempt(t,e,r){const o=await this.readState(r),s=o.recovery_history??[],n=s.findIndex(v=>v.recovery_step_id_tokenized===t);if(n===-1){this.logger.warn({event:"recovery_resolve_orphan",attempt_id:t},"recovery_resolved op had no matching attempt; skipping");return}const c=s[n];if(c.outcome===e&&c.finished_at){this.logger.info({event:"recovery_resolved_duplicate",attempt_id:t,outcome:e},"duplicate recovery_resolved op; skipping");return}const l=s.slice();l[n]={...c,outcome:e,finished_at:r};const d={...o,recovery_history:l};await this.writeState(d),this.logger.info({event:"recovery_resolved_persisted",attempt_id:t,outcome:e},"recovery attempt resolved")}async acquireLock(t,e){const r={...t,pid:process.pid,hostname:h(),ownerKind:"client",startedAt:t.startedAt||e,leaseId:t.leaseId??P(t.sessionId,e),leaseState:"active",leaseAcquiredAt:t.leaseAcquiredAt??e,leaseDurationMs:t.leaseDurationMs??E,leaseExpiresAt:t.leaseExpiresAt??t.ttlExpiresAt},o=JSON.stringify(r,null,2);try{const s=await a.open(this.lockPath,"wx");try{await s.writeFile(o,"utf8")}finally{await s.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired",pid:r.pid,sessionId:r.sessionId},"yoloop lock acquired");return}catch(s){const n=s;if(n.code!=="EEXIST")throw this.logger.error({event:"yoloop_lock_acquire_error",err:n.message},"failed to acquire yoloop lock"),s;const c=await this.readLock();if(c&&this.isSameClientSessionLock(c,r)){this.logger.info({event:"yoloop_lock_acquire_duplicate",sessionId:r.sessionId},"duplicate acquire_lock for current yoloop session; treating as no-op");return}if(!await this.maybeRecoverOrphanLock(e))throw this.logger.warn({event:"yoloop_lock_held"},"yoloop lock held by another live process"),s;const d=await a.open(this.lockPath,"wx");try{await d.writeFile(o,"utf8")}finally{await d.close()}this.trySetFilePerms(this.lockPath),this.logger.info({event:"yoloop_lock_acquired_after_recovery",pid:r.pid},"yoloop lock acquired after orphan recovery")}}async releaseLock(){try{await a.unlink(this.lockPath),this.logger.info({event:"yoloop_lock_released"},"yoloop lock released")}catch(t){const e=t;if(e.code==="ENOENT"){this.logger.info({event:"yoloop_lock_release_noop"},"yoloop lock release noop (file absent)");return}this.logger.warn({event:"yoloop_lock_release_error",err:e.message},"unexpected error releasing yoloop lock")}}async readLock(){try{const t=await a.readFile(this.lockPath,"utf8"),e=JSON.parse(t),r=e.currentEpicId??e.epicId??"";return{pid:e.pid??0,hostname:e.hostname??"",...e.ownerKind?{ownerKind:e.ownerKind}:{},...e.ownerPidState?{ownerPidState:e.ownerPidState}:{},...typeof e.leaseId=="string"?{leaseId:e.leaseId}:{},...e.leaseState?{leaseState:e.leaseState}:{},...typeof e.leaseAcquiredAt=="string"?{leaseAcquiredAt:e.leaseAcquiredAt}:{},...typeof e.leaseDurationMs=="number"?{leaseDurationMs:e.leaseDurationMs}:{},...typeof e.leaseExpiresAt=="string"?{leaseExpiresAt:e.leaseExpiresAt}:{},startedAt:e.startedAt??"",sessionId:e.sessionId??"",mode:e.mode??"epic-id",inputPath:e.inputPath??"",epicQueue:e.epicQueue??(r?[r]:[]),currentEpicIndex:e.currentEpicIndex??0,currentEpicId:r,ttlExpiresAt:e.ttlExpiresAt??"",epicId:e.epicId??r}}catch{return null}}async readState(t){try{const e=await a.readFile(this.statePath,"utf8"),r=JSON.parse(e);return L(r,t)}catch(e){const r=e;return r.code!=="ENOENT"&&this.logger.warn({event:"yoloop_state_read_error",err:r.message},"yoloop state file unreadable; using defaults"),u(t)}}async writeState(t){const e=`${this.statePath}.tmp.${process.pid}.${Date.now()}`,r=JSON.stringify(t,null,2);try{await a.writeFile(e,r,"utf8"),this.trySetFilePerms(e),await a.rename(e,this.statePath)}catch(o){const s=o;this.logger.error({event:"yoloop_state_write_error",err:s.message},"failed to write yoloop state");try{await a.unlink(e)}catch{}throw o}}async advanceEpic(t,e,r){const o=await this.readState(r);if(!o.currentSession){this.logger.warn({event:"yoloop_advance_no_session",nextEpicId:e},"advance_epic with no active session; skipping");return}const s={...o,currentSession:{...o.currentSession,currentEpicIndex:t}};await this.writeState(s),this.logger.info({event:"yoloop_epic_advance",nextEpicId:e,nextIndex:t},"yoloop advanced to next epic")}async ensureDir(){try{await a.mkdir(this.stateDir,{recursive:!0}),this.trySetDirPerms(this.stateDir)}catch(t){const e=t;throw this.logger.error({event:"yoloop_mkdir_error",err:e.message,path:this.stateDir},"failed to ensure .neocortex/ directory"),t}}trySetFilePerms(t){try{k(t)}catch{}}trySetDirPerms(t){try{m(t)}catch{}}async prepareLockForSnapshot(t,e){if(!t)return null;if(t.ownerKind!=="client")return t;const r=f(t,e),o={...t,leaseState:r};if(r==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"lease_expired"},"expired client-owned yoloop lock lease recovered before snapshot echo"),null;if(!o.pid||o.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"invalid_pid"},"invalid PID in client-owned yoloop lock; recovered"),null;if(o.hostname&&o.hostname!==h())return{...o,ownerPidState:"unknown"};try{return process.kill(o.pid,0),{...o,ownerPidState:"live"}}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_client_lock_recovered",reason:"dead_pid"},"stale client-owned yoloop lock recovered before snapshot echo"),null}}async maybeRecoverOrphanLock(t){const e=await this.readLock();if(!e)return!0;if(f(e,t)==="expired")return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"lease_expired"},"expired yoloop lock lease recovered"),!0;if(!e.pid||e.pid<=0)return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"invalid_pid"},"invalid PID in lock file; recovered"),!0;if(e.hostname&&e.hostname!==h())return this.logger.warn({event:"yoloop_lock_remote_host",ownerKind:e.ownerKind??"server",ownerPidState:"unknown"},"lock held by a remote host; honoring"),!1;try{return process.kill(e.pid,0),!1}catch{return await this.deleteLockBestEffort(),this.logger.warn({event:"yoloop_orphan_lock_recovered",reason:"dead_pid"},"orphan yoloop lock recovered"),!0}}async deleteLockBestEffort(){try{await a.unlink(this.lockPath)}catch{}}isSameClientSessionLock(t,e){return t.ownerKind==="client"&&t.sessionId===e.sessionId&&t.pid===process.pid&&t.hostname===h()}async cleanupOrphanTempFiles(){try{const t=g(this.statePath),e=await a.readdir(t),r=`${_}.tmp.`,o=e.filter(s=>s.startsWith(r));for(const s of o)try{await a.unlink(p(t,s))}catch{}}catch{}}}function P(i,t){return`lease-${x(`${i}:${t}`)}`}function x(i){let t=2166136261;for(let e=0;e<i.length;e+=1)t^=i.charCodeAt(e),t=Math.imul(t,16777619)>>>0;return t.toString(36).padStart(7,"0")}function f(i,t){const e=i.leaseExpiresAt??i.ttlExpiresAt,r=Date.parse(e),o=Date.parse(t);return!Number.isFinite(r)||!Number.isFinite(o)?"unknown":r<=o?"expired":"active"}function L(i,t){const e=u(t);return{version:"1.1",lastResetAt:i.lastResetAt??e.lastResetAt,storiesRunToday:i.storiesRunToday??0,iterationsToday:i.iterationsToday??0,iterationsTodayDate:i.iterationsTodayDate??e.iterationsTodayDate,tokensUsedToday:i.tokensUsedToday??0,lastRunAt:i.lastRunAt??null,coolDownUntil:i.coolDownUntil??null,totalRunsAllTime:i.totalRunsAllTime??0,totalStoriesAllTime:i.totalStoriesAllTime??0,currentSession:i.currentSession??null,recovery_history:Array.isArray(i.recovery_history)?w(i.recovery_history):[]}}function w(i){return i.length<=y?i:i.slice(i.length-y)}export{b as YoloopClientStateStore,w as capRecoveryHistory,u as createDefaultCounters,q as createDefaultState};
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};