@ornexus/neocortex 4.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +56 -0
  2. package/LICENSE-COMMERCIAL.md +70 -0
  3. package/README.md +58 -0
  4. package/dist/sbom.cdx.json +7067 -0
  5. package/docs/install/coderabbit-manual-setup.md +86 -0
  6. package/docs/install/installer-diagnostics.md +107 -0
  7. package/docs/install/linux-global-install.md +97 -0
  8. package/install.js +572 -0
  9. package/install.ps1 +2214 -0
  10. package/install.sh +2013 -0
  11. package/package.json +118 -0
  12. package/packages/client/dist/adapters/adapter-registry.d.ts +61 -0
  13. package/packages/client/dist/adapters/adapter-registry.js +1 -0
  14. package/packages/client/dist/adapters/antigravity-adapter.d.ts +18 -0
  15. package/packages/client/dist/adapters/antigravity-adapter.js +2 -0
  16. package/packages/client/dist/adapters/claude-code-adapter.d.ts +19 -0
  17. package/packages/client/dist/adapters/claude-code-adapter.js +3 -0
  18. package/packages/client/dist/adapters/codex-adapter.d.ts +19 -0
  19. package/packages/client/dist/adapters/codex-adapter.js +2 -0
  20. package/packages/client/dist/adapters/cursor-adapter.d.ts +19 -0
  21. package/packages/client/dist/adapters/cursor-adapter.js +4 -0
  22. package/packages/client/dist/adapters/gemini-adapter.d.ts +18 -0
  23. package/packages/client/dist/adapters/gemini-adapter.js +2 -0
  24. package/packages/client/dist/adapters/index.d.ts +19 -0
  25. package/packages/client/dist/adapters/index.js +1 -0
  26. package/packages/client/dist/adapters/platform-detector.d.ts +48 -0
  27. package/packages/client/dist/adapters/platform-detector.js +1 -0
  28. package/packages/client/dist/adapters/target-adapter.d.ts +70 -0
  29. package/packages/client/dist/adapters/target-adapter.js +0 -0
  30. package/packages/client/dist/adapters/vscode-adapter.d.ts +19 -0
  31. package/packages/client/dist/adapters/vscode-adapter.js +2 -0
  32. package/packages/client/dist/agent/refresh-stubs.d.ts +80 -0
  33. package/packages/client/dist/agent/refresh-stubs.js +2 -0
  34. package/packages/client/dist/agent/update-agent-yaml.d.ts +26 -0
  35. package/packages/client/dist/agent/update-agent-yaml.js +1 -0
  36. package/packages/client/dist/agent/update-description.d.ts +45 -0
  37. package/packages/client/dist/agent/update-description.js +1 -0
  38. package/packages/client/dist/cache/crypto-utils.d.ts +30 -0
  39. package/packages/client/dist/cache/crypto-utils.js +1 -0
  40. package/packages/client/dist/cache/encrypted-cache.d.ts +30 -0
  41. package/packages/client/dist/cache/encrypted-cache.js +1 -0
  42. package/packages/client/dist/cache/in-memory-asset-cache.d.ts +62 -0
  43. package/packages/client/dist/cache/in-memory-asset-cache.js +1 -0
  44. package/packages/client/dist/cache/index.d.ts +13 -0
  45. package/packages/client/dist/cache/index.js +1 -0
  46. package/packages/client/dist/cache/protected-pi-boundary.d.ts +19 -0
  47. package/packages/client/dist/cache/protected-pi-boundary.js +1 -0
  48. package/packages/client/dist/checkpoint/checkpoint-client-reader.d.ts +45 -0
  49. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -0
  50. package/packages/client/dist/checkpoint/index.d.ts +12 -0
  51. package/packages/client/dist/checkpoint/index.js +1 -0
  52. package/packages/client/dist/checkpoint/shared-checkpoint-types.d.ts +85 -0
  53. package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -0
  54. package/packages/client/dist/cli.d.ts +14 -0
  55. package/packages/client/dist/cli.js +48 -0
  56. package/packages/client/dist/commands/activate.d.ts +55 -0
  57. package/packages/client/dist/commands/activate.js +8 -0
  58. package/packages/client/dist/commands/cache-status.d.ts +39 -0
  59. package/packages/client/dist/commands/cache-status.js +2 -0
  60. package/packages/client/dist/commands/invoke.d.ts +229 -0
  61. package/packages/client/dist/commands/invoke.js +63 -0
  62. package/packages/client/dist/commands/refresh-memory.d.ts +11 -0
  63. package/packages/client/dist/commands/refresh-memory.js +1 -0
  64. package/packages/client/dist/config/resolver-selection.d.ts +40 -0
  65. package/packages/client/dist/config/resolver-selection.js +1 -0
  66. package/packages/client/dist/config/secure-config.d.ts +78 -0
  67. package/packages/client/dist/config/secure-config.js +12 -0
  68. package/packages/client/dist/constants.d.ts +25 -0
  69. package/packages/client/dist/constants.js +1 -0
  70. package/packages/client/dist/context/context-collector.d.ts +28 -0
  71. package/packages/client/dist/context/context-collector.js +2 -0
  72. package/packages/client/dist/context/context-sanitizer.d.ts +28 -0
  73. package/packages/client/dist/context/context-sanitizer.js +1 -0
  74. package/packages/client/dist/continuity/continuity-client-state-store.d.ts +183 -0
  75. package/packages/client/dist/continuity/continuity-client-state-store.js +1 -0
  76. package/packages/client/dist/continuity/invoke-hooks.d.ts +18 -0
  77. package/packages/client/dist/continuity/invoke-hooks.js +1 -0
  78. package/packages/client/dist/continuity/migrations/001-initial-schema.d.ts +11 -0
  79. package/packages/client/dist/continuity/migrations/001-initial-schema.js +263 -0
  80. package/packages/client/dist/continuity/sqlite-store.d.ts +409 -0
  81. package/packages/client/dist/continuity/sqlite-store.js +226 -0
  82. package/packages/client/dist/errors/error-messages.d.ts +40 -0
  83. package/packages/client/dist/errors/error-messages.js +2 -0
  84. package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +31 -0
  85. package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -0
  86. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.d.ts +77 -0
  87. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -0
  88. package/packages/client/dist/i18n/first-run.d.ts +23 -0
  89. package/packages/client/dist/i18n/first-run.js +2 -0
  90. package/packages/client/dist/index.d.ts +56 -0
  91. package/packages/client/dist/index.js +1 -0
  92. package/packages/client/dist/license/index.d.ts +5 -0
  93. package/packages/client/dist/license/index.js +1 -0
  94. package/packages/client/dist/license/license-client.d.ts +79 -0
  95. package/packages/client/dist/license/license-client.js +1 -0
  96. package/packages/client/dist/machine/fingerprint.d.ts +34 -0
  97. package/packages/client/dist/machine/fingerprint.js +2 -0
  98. package/packages/client/dist/machine/index.d.ts +5 -0
  99. package/packages/client/dist/machine/index.js +1 -0
  100. package/packages/client/dist/memory/project-memory-writer.d.ts +74 -0
  101. package/packages/client/dist/memory/project-memory-writer.js +36 -0
  102. package/packages/client/dist/memory/shared-project-memory-types.d.ts +370 -0
  103. package/packages/client/dist/memory/shared-project-memory-types.js +2 -0
  104. package/packages/client/dist/policy/architecture-policy.d.ts +40 -0
  105. package/packages/client/dist/policy/architecture-policy.js +2 -0
  106. package/packages/client/dist/policy/index.d.ts +8 -0
  107. package/packages/client/dist/policy/index.js +1 -0
  108. package/packages/client/dist/policy/shared-policy-types.d.ts +89 -0
  109. package/packages/client/dist/policy/shared-policy-types.js +0 -0
  110. package/packages/client/dist/resilience/circuit-breaker.d.ts +70 -0
  111. package/packages/client/dist/resilience/circuit-breaker.js +1 -0
  112. package/packages/client/dist/resilience/degradation-manager.d.ts +67 -0
  113. package/packages/client/dist/resilience/degradation-manager.js +1 -0
  114. package/packages/client/dist/resilience/freshness-indicator.d.ts +59 -0
  115. package/packages/client/dist/resilience/freshness-indicator.js +1 -0
  116. package/packages/client/dist/resilience/index.d.ts +8 -0
  117. package/packages/client/dist/resilience/index.js +1 -0
  118. package/packages/client/dist/resilience/recovery-detector.d.ts +59 -0
  119. package/packages/client/dist/resilience/recovery-detector.js +1 -0
  120. package/packages/client/dist/resolvers/asset-resolver.d.ts +79 -0
  121. package/packages/client/dist/resolvers/asset-resolver.js +0 -0
  122. package/packages/client/dist/resolvers/local-resolver.d.ts +26 -0
  123. package/packages/client/dist/resolvers/local-resolver.js +8 -0
  124. package/packages/client/dist/resolvers/remote-resolver.d.ts +91 -0
  125. package/packages/client/dist/resolvers/remote-resolver.js +1 -0
  126. package/packages/client/dist/runner/cli.d.ts +121 -0
  127. package/packages/client/dist/runner/cli.js +20 -0
  128. package/packages/client/dist/runner/scheduler.d.ts +116 -0
  129. package/packages/client/dist/runner/scheduler.js +6 -0
  130. package/packages/client/dist/runner-cli.d.ts +9 -0
  131. package/packages/client/dist/runner-cli.js +3 -0
  132. package/packages/client/dist/state/project-state-snapshot.d.ts +15 -0
  133. package/packages/client/dist/state/project-state-snapshot.js +1 -0
  134. package/packages/client/dist/state/state-json-repair.d.ts +17 -0
  135. package/packages/client/dist/state/state-json-repair.js +3 -0
  136. package/packages/client/dist/telemetry/index.d.ts +5 -0
  137. package/packages/client/dist/telemetry/index.js +1 -0
  138. package/packages/client/dist/telemetry/offline-queue.d.ts +57 -0
  139. package/packages/client/dist/telemetry/offline-queue.js +1 -0
  140. package/packages/client/dist/tier/index.d.ts +5 -0
  141. package/packages/client/dist/tier/index.js +1 -0
  142. package/packages/client/dist/tier/tier-aware-client.d.ts +105 -0
  143. package/packages/client/dist/tier/tier-aware-client.js +1 -0
  144. package/packages/client/dist/types/index.d.ts +140 -0
  145. package/packages/client/dist/types/index.js +1 -0
  146. package/packages/client/dist/yoloop/discovery-hook.d.ts +85 -0
  147. package/packages/client/dist/yoloop/discovery-hook.js +2 -0
  148. package/packages/client/dist/yoloop/index.d.ts +10 -0
  149. package/packages/client/dist/yoloop/index.js +1 -0
  150. package/packages/client/dist/yoloop/invoke-hooks.d.ts +125 -0
  151. package/packages/client/dist/yoloop/invoke-hooks.js +5 -0
  152. package/packages/client/dist/yoloop/shared-discover-epics.d.ts +289 -0
  153. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -0
  154. package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +172 -0
  155. package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -0
  156. package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +124 -0
  157. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -0
  158. package/postinstall.js +754 -0
  159. package/targets-stubs/antigravity/README.md +36 -0
  160. package/targets-stubs/antigravity/gemini.md +29 -0
  161. package/targets-stubs/antigravity/install-antigravity.sh +153 -0
  162. package/targets-stubs/antigravity/mcp-config.json +30 -0
  163. package/targets-stubs/antigravity/skill/SKILL.md +159 -0
  164. package/targets-stubs/claude-code/.mcp.json +32 -0
  165. package/targets-stubs/claude-code/README.md +20 -0
  166. package/targets-stubs/claude-code/neocortex-root.agent.yaml +42 -0
  167. package/targets-stubs/claude-code/neocortex-root.md +310 -0
  168. package/targets-stubs/claude-code/neocortex.agent.yaml +42 -0
  169. package/targets-stubs/claude-code/neocortex.md +378 -0
  170. package/targets-stubs/codex/AGENTS.md +244 -0
  171. package/targets-stubs/codex/README.md +47 -0
  172. package/targets-stubs/codex/config-mcp.toml +22 -0
  173. package/targets-stubs/codex/install-codex.sh +63 -0
  174. package/targets-stubs/codex/neocortex.toml +29 -0
  175. package/targets-stubs/cursor/README.md +33 -0
  176. package/targets-stubs/cursor/agent.md +204 -0
  177. package/targets-stubs/cursor/install-cursor.sh +50 -0
  178. package/targets-stubs/cursor/mcp.json +30 -0
  179. package/targets-stubs/gemini-cli/README.md +34 -0
  180. package/targets-stubs/gemini-cli/agent.md +234 -0
  181. package/targets-stubs/gemini-cli/agents/neocortex.md +54 -0
  182. package/targets-stubs/gemini-cli/gemini.md +46 -0
  183. package/targets-stubs/gemini-cli/install-gemini.sh +70 -0
  184. package/targets-stubs/gemini-cli/settings-mcp.json +30 -0
  185. package/targets-stubs/kimi/mcp.json +33 -0
  186. package/targets-stubs/kimi/neocortex.md +54 -0
  187. package/targets-stubs/lib/mcp-merge.js +189 -0
  188. package/targets-stubs/openclaw/README.md +12 -0
  189. package/targets-stubs/openclaw/SKILL.md +88 -0
  190. package/targets-stubs/opencode/neocortex-root.md +261 -0
  191. package/targets-stubs/opencode/neocortex.md +59 -0
  192. package/targets-stubs/opencode/opencode-mcp.json +35 -0
  193. package/targets-stubs/vscode/README.md +34 -0
  194. package/targets-stubs/vscode/copilot-instructions.md +47 -0
  195. package/targets-stubs/vscode/install-vscode.sh +72 -0
  196. package/targets-stubs/vscode/mcp.json +36 -0
  197. package/targets-stubs/vscode/neocortex.agent.md +245 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * Logger contract -- structural, accepts any object with optional `warn` /
15
+ * `info` methods. Allows console, pino-style, or test stubs.
16
+ */
17
+ export interface DiscoveryHookLogger {
18
+ warn?(message: string): void;
19
+ info?(message: string): void;
20
+ }
21
+ /**
22
+ * Input context for the hook. `stateSnapshot` is mutated in place when
23
+ * discovery applies (P101.07 pattern).
24
+ */
25
+ export interface DiscoveryHookContext {
26
+ /** Raw trigger args (e.g., `"*yoloop @docs/epics/"`). */
27
+ readonly args: string;
28
+ /** Absolute path to the consumer project root. */
29
+ readonly projectRoot: string;
30
+ /** State snapshot collected by `collectStateSnapshot` -- mutated in place. */
31
+ readonly stateSnapshot: {
32
+ epics?: Record<string, unknown>;
33
+ stories?: Record<string, unknown>;
34
+ } & Record<string, unknown>;
35
+ /** YOLOOP_AUTO_DISCOVERY flag value (operator opt-in). */
36
+ readonly featureFlag: boolean;
37
+ /** Optional logger (defaults to no-op). */
38
+ readonly logger?: DiscoveryHookLogger;
39
+ }
40
+ /**
41
+ * Output of the hook. `applied: true` means discovery + merge ran
42
+ * successfully and `stateSnapshot` was mutated.
43
+ */
44
+ export interface DiscoveryHookResult {
45
+ /** True if discovery + merge applied; false if hook was a no-op. */
46
+ readonly applied: boolean;
47
+ /** IDs of epics newly added to `state.epics` (empty if !applied). */
48
+ readonly added: ReadonlyArray<string>;
49
+ /** Epics skipped by merge (e.g., create-epic preserved). */
50
+ readonly skipped: ReadonlyArray<{
51
+ id: string;
52
+ reason: string;
53
+ }>;
54
+ /** Number of stories added to `state.stories`. */
55
+ readonly storiesAdded: number;
56
+ /** Reason hook was a no-op (only present if !applied). */
57
+ readonly reason?: string;
58
+ }
59
+ /**
60
+ * Run discovery + merge if hook conditions are met.
61
+ *
62
+ * Async because `buildDefaultDiscoveryFs()` lazy-imports `node:fs` /
63
+ * `node:path` (tree-shake friendly per P120.01 design). Returns
64
+ * synchronously-resolvable promise in the no-op cases (flag off, non-yoloop
65
+ * trigger, file not dir, missing dir) -- discovery work only happens on
66
+ * the happy path.
67
+ *
68
+ * **Never throws**. ANY error is caught, logged via `logger.warn`, and
69
+ * returned as `{ applied: false, reason }`. Caller continues normal
70
+ * invoke flow without enrichment.
71
+ *
72
+ * @param ctx hook context (args + projectRoot + stateSnapshot + flag + logger)
73
+ * @returns DiscoveryHookResult with telemetry counters
74
+ */
75
+ export declare function runDiscoveryHook(ctx: DiscoveryHookContext): Promise<DiscoveryHookResult>;
76
+ /**
77
+ * Async wrapper with lazy-load semantics + outermost defensive try/catch.
78
+ *
79
+ * Designed to be invoked from `invoke-hooks.ts`. Performs the same
80
+ * `featureFlag` short-circuit as `runDiscoveryHook` to keep the async
81
+ * import truly tree-shakeable when the flag is off. Wraps the inner call
82
+ * in an additional try/catch as defense-in-depth (the hook itself is
83
+ * already fail-open, but module-load errors are caught here).
84
+ */
85
+ export declare function maybeRunDiscoveryHook(ctx: DiscoveryHookContext): Promise<DiscoveryHookResult>;
@@ -0,0 +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};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Epic P101.06 -- Client-side yoloop state persistence.
3
+ *
4
+ * Re-exports the YoloopClientStateStore class and wire-format types for
5
+ * consumption by invoke.ts (P101.07 pre/post-request hooks).
6
+ */
7
+ export { YoloopClientStateStore, createDefaultCounters, createDefaultState, type ClientYoloopLogger, } from './yoloop-client-state-store.js';
8
+ export { isYoloopTrigger, readYoloopForRequest, persistYoloopFromResponse, maybeRunDiscoveryHook, } from './invoke-hooks.js';
9
+ export type { DiscoveryHookContext, DiscoveryHookResult, DiscoveryHookLogger, } from './discovery-hook.js';
10
+ export type { YoloopPersistedCounters, YoloopPersistedLock, YoloopPersistedSession, YoloopPersistedState, YoloopSessionMode, YoloopStateOperation, YoloopStateUpdate, } from './shared-yoloop-types.js';
@@ -0,0 +1 @@
1
+ import{YoloopClientStateStore as r,createDefaultCounters as t,createDefaultState as a}from"./yoloop-client-state-store.js";import{isYoloopTrigger as s,readYoloopForRequest as l,persistYoloopFromResponse as i,maybeRunDiscoveryHook as u}from"./invoke-hooks.js";export{r as YoloopClientStateStore,t as createDefaultCounters,a as createDefaultState,s as isYoloopTrigger,u as maybeRunDiscoveryHook,i as persistYoloopFromResponse,l as readYoloopForRequest};
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Epic P101.07 -- invoke.ts hooks for yoloop state persistence.
3
+ *
4
+ * Two hooks:
5
+ * - `readYoloopForRequest(args, projectRoot)` — pre-request; returns
6
+ * `YoloopPersistedState | undefined` to embed in stateSnapshot.yoloop.
7
+ * Returns undefined (no-op) for non-yoloop invocations to avoid I/O.
8
+ * - `persistYoloopFromResponse(metadata, projectRoot)` — post-response;
9
+ * if the server returned `yoloopStateUpdate`, apply operations locally.
10
+ * Fail-soft: warns to stderr on error but never throws.
11
+ *
12
+ * Trigger detection is regex-based over the first token of `args`, aligned
13
+ * with the P96.01 TRIGGER_ALIASES set for yoloop. Cheap enough to run on
14
+ * every invoke.
15
+ */
16
+ import type { YoloopPersistedState } from './shared-yoloop-types.js';
17
+ import type { DiscoveryHookContext, DiscoveryHookResult } from './discovery-hook.js';
18
+ import type { GraphRetrievalHookContext, GraphRetrievalHookResult } from '../graph-retrieval/pre-command-hook.js';
19
+ /**
20
+ * Detect whether an args string is a yoloop invocation (including aliases).
21
+ * Matches: `*yoloop`, `yoloop`, `*auto-yolo`, `*epic-runner`, `*story-runner`,
22
+ * `*loop-yolo`, `*auto-epic` (case-insensitive, with optional leading whitespace
23
+ * and optional `*` prefix).
24
+ */
25
+ export declare function isYoloopTrigger(args: string): boolean;
26
+ /**
27
+ * Input context for `maybeAutoResolveYoloopArgs`.
28
+ *
29
+ * `args` is mutated in place. Caller (invoke.ts) reads the rewritten value
30
+ * after the hook returns. `projectRoot` and `logger` are read-only.
31
+ */
32
+ export interface AutoResolveYoloopContext {
33
+ /** Raw args. Mutated in place when auto-resolve fires. */
34
+ args: string;
35
+ /** Absolute path to the consumer project root. */
36
+ readonly projectRoot: string;
37
+ /** Optional structural logger (info/warn). Defaults to no-op. */
38
+ readonly logger?: {
39
+ info?: (msg: string) => void;
40
+ warn?: (msg: string) => void;
41
+ };
42
+ }
43
+ /**
44
+ * P121.05 -- Zero-config `*yoloop`. When the user types `*yoloop` (or any
45
+ * alias) with no args, auto-resolve to `<projectRoot>/docs/epics/` if that
46
+ * directory exists.
47
+ *
48
+ * Behavior:
49
+ * - Yoloop trigger detection: same regex as P101.07 / P120.04.
50
+ * - Args-empty detection: trigger token + optional whitespace; remainder
51
+ * `=== ''` after `.trim()`.
52
+ * - Directory existence: synchronous `fs.statSync(...).isDirectory()`,
53
+ * wrapped in try/catch.
54
+ * - Rewrite: appends ` @docs/epics/` to the matched trigger token,
55
+ * preserving the original token (canonical or alias) and any leading
56
+ * `*` prefix.
57
+ * - Fail-soft: any error -> `ctx.logger.warn` + return unchanged args.
58
+ *
59
+ * Mutates `ctx.args` in place when auto-resolve fires. Caller (invoke.ts)
60
+ * propagates the new value via the InvokeOptions immutable update pattern.
61
+ *
62
+ * AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC13.
63
+ */
64
+ export declare function maybeAutoResolveYoloopArgs(ctx: AutoResolveYoloopContext): void;
65
+ /**
66
+ * Pre-request hook: read client-side yoloop state for the given project.
67
+ * Returns undefined for non-yoloop invocations (no I/O). Returns undefined
68
+ * on read error (fail-soft -- server treats as fresh start).
69
+ */
70
+ export declare function readYoloopForRequest(args: string, projectRoot: string, nowIso?: string): Promise<YoloopPersistedState | undefined>;
71
+ /**
72
+ * Post-response hook: apply server-provided operations to local state.
73
+ * Safe to call for any response — no-op if no yoloopStateUpdate present.
74
+ * Fail-soft: warns to stderr on error, never throws (the agent already
75
+ * received instructions; next invoke self-heals via stateSnapshot.yoloop).
76
+ */
77
+ export declare function persistYoloopFromResponse(metadata: Record<string, unknown> | undefined, projectRoot: string, nowIso?: string): Promise<void>;
78
+ /**
79
+ * Epic P125.03 -- Post-response hook: persist server-migrated state back
80
+ * to `.neocortex/state.json` via atomic temp+rename.
81
+ *
82
+ * Triggered when `metadata.stateMutated === true` AND
83
+ * `metadata.migratedState` is a plain object. The server detected legacy
84
+ * `planning.*` schema in the incoming `stateSnapshot`, ran the
85
+ * `migrateStateJson` (P103.03) pure-fn in memory, and shipped the canonical
86
+ * v1 payload back via metadata. This hook writes that payload verbatim to
87
+ * the client filesystem.
88
+ *
89
+ * P101 ADR preserved: the server NEVER touches client filesystem -- the
90
+ * mutation is computed server-side and persisted client-side. The hook is
91
+ * the closing half of the contract.
92
+ *
93
+ * Atomicity: writes to `.neocortex/state.json.tmp.<random>` first, then
94
+ * renames into place (POSIX atomic). Guarantees no partial-write corruption
95
+ * on crash mid-write. Mirrors `YoloopClientStateStore.writeState()`
96
+ * pattern.
97
+ *
98
+ * Fail-soft: any error -> warn to stderr + return. Never throws (caller
99
+ * already received instructions; next invoke self-heals -- server re-runs
100
+ * migrate idempotently and re-emits the same payload).
101
+ */
102
+ export declare function persistMigratedStateFromResponse(metadata: Record<string, unknown> | undefined, projectRoot: string): Promise<void>;
103
+ /**
104
+ * P120.04: lazy-loaded discovery hook wrapper.
105
+ *
106
+ * Short-circuits BEFORE the dynamic import when `featureFlag` is off, so
107
+ * clients with `YOLOOP_AUTO_DISCOVERY` unset (default) never load the
108
+ * discovery-hook module. ESM tree-shakers can prune the entire module +
109
+ * its `shared-discover-epics.ts` mirror in production builds.
110
+ *
111
+ * Defensive try/catch around the dynamic import handles the
112
+ * (vanishingly unlikely) case where the hook module is missing or the
113
+ * dynamic import itself throws -- never blocks the invoke flow.
114
+ *
115
+ * Caller (invoke.ts) is expected to ALSO wrap this in a try/catch as
116
+ * belt-and-suspenders.
117
+ */
118
+ export declare function maybeRunDiscoveryHook(ctx: DiscoveryHookContext): Promise<DiscoveryHookResult>;
119
+ /**
120
+ * P158.06: lazy-loaded graph retrieval wrapper.
121
+ *
122
+ * Short-circuits before dynamic import when the feature flag is off. Runtime
123
+ * errors are public-warning/fail-soft and never block invoke.
124
+ */
125
+ export declare function maybeRunGraphRetrievalHook(ctx: GraphRetrievalHookContext): Promise<GraphRetrievalHookResult>;
@@ -0,0 +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)}.
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};
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * P120.04: Inlined byte-identical mirror from
15
+ * `@neocortex/shared/yoloop/discover-epics`.
16
+ *
17
+ * Same regression class as P117.01 (checkpoint inline) + P117.07 (yoloop types
18
+ * inline) + P119.02 (policy types inline). Mirrors P92.01 server SSoT-Shadow
19
+ * precedent.
20
+ *
21
+ * Live regression: client tarball is published standalone via `@ornexus/neocortex`
22
+ * and does NOT include `@neocortex/shared` as a runtime dependency (workspace
23
+ * package, excluded from tarball per `.npmignore` + `tsconfig.build.json`). Any
24
+ * non-erased import of `@neocortex/shared` in `packages/client/dist/yoloop/**`
25
+ * causes runtime `ERR_MODULE_NOT_FOUND` on globally installed `@ornexus/neocortex`.
26
+ * For P120.04 this would silently disable the discovery hook, defeating the
27
+ * P120 epic premise (filesystem epics deserve to run).
28
+ *
29
+ * Sync responsibility: `discover-shared-sync.test.ts` (client-side drift test,
30
+ * AC equivalent of P117.01/P117.07/P119.02 drift tests).
31
+ *
32
+ * Defense-in-depth: `validate-pre-publish.js` ships `validateClientNoSharedImport()`
33
+ * (P117.01 expanded by P118.01 to whole-tree) — match = exit 1 (publish blocked).
34
+ *
35
+ * DO NOT EDIT WITHOUT also editing `packages/shared/src/yoloop/discover-epics.ts`.
36
+ *
37
+ * Pattern references:
38
+ * - P92.01 (SSoT-Shadow canonical) — server inline mirror precedent
39
+ * - P117.01 (checkpoint inline) — first client-side application
40
+ * - P117.07 (yoloop types inline) — second application
41
+ * - P119.02 (policy types inline) — third application
42
+ * - P120.01 (discoverEpicsFromDir SSoT) — source of this mirror
43
+ * - P120.02 (mergeDiscoveredIntoState SSoT) — source of this mirror
44
+ */
45
+ /**
46
+ * Source of the canonical epic ID resolution.
47
+ *
48
+ * - `'frontmatter'`: `id:` field in YAML frontmatter (highest priority)
49
+ * - `'filename-canonical'`: matched `EPIC_FILE_PATTERNS[0]` or `[1]` (numeric)
50
+ * - `'filename-slug'`: matched `EPIC_FILE_PATTERNS[2]` (alpha-slug fallback)
51
+ */
52
+ export type EpicIdSource = 'frontmatter' | 'filename-canonical' | 'filename-slug';
53
+ /**
54
+ * Descriptor for a discovered epic file.
55
+ * Wire-format public (consumed by P120.02 merge fn + admin observability).
56
+ */
57
+ export interface EpicDescriptor {
58
+ /** Canonical ID (e.g., "P119", "01", "customer-portal"). */
59
+ readonly id: string;
60
+ /** Basename only (e.g., "epic-P119.md"). */
61
+ readonly filename: string;
62
+ /** Path relative to the scan dir. */
63
+ readonly path: string;
64
+ /** Optional title from frontmatter. */
65
+ readonly title?: string;
66
+ /** Optional status from frontmatter. */
67
+ readonly status?: string;
68
+ /** Optional epic dependencies from frontmatter. */
69
+ readonly depends_on?: readonly string[];
70
+ /** Provenance marker -- always 'discovered' from this fn. */
71
+ readonly source: 'discovered';
72
+ /** ISO 8601 timestamp at discovery time. */
73
+ readonly discovered_at: string;
74
+ /** Stories linked to this epic via filename pattern OR frontmatter epic_id. */
75
+ readonly stories: ReadonlyArray<StoryDescriptor>;
76
+ /** Internal: how was the canonical ID resolved (debugging + tests). */
77
+ readonly _idSource: EpicIdSource;
78
+ }
79
+ /**
80
+ * Descriptor for a discovered story file.
81
+ * Stories are attached to their owning epic via `EpicDescriptor.stories[]`.
82
+ */
83
+ export interface StoryDescriptor {
84
+ /** Canonical (e.g., "P119.01", "01.02"). */
85
+ readonly id: string;
86
+ readonly epic_id: string;
87
+ readonly filename: string;
88
+ readonly path: string;
89
+ readonly title?: string;
90
+ readonly status?: string;
91
+ readonly depends_on?: readonly string[];
92
+ readonly files_to_modify?: readonly string[];
93
+ }
94
+ /**
95
+ * Output shape for `discoverEpicsFromDir`.
96
+ * `warnings` and `skipped` are non-fatal observability data.
97
+ */
98
+ export interface DiscoveryResult {
99
+ readonly epics: ReadonlyArray<EpicDescriptor>;
100
+ readonly warnings: ReadonlyArray<string>;
101
+ readonly skipped: ReadonlyArray<string>;
102
+ }
103
+ /**
104
+ * Filesystem abstraction injected into the discovery fn.
105
+ * Caller wraps `node:fs` (server / client hook) or supplies an in-memory
106
+ * mock (tests). Adapter is fail-open: methods MUST swallow IO errors.
107
+ */
108
+ export interface DiscoveryFsAdapter {
109
+ /** Returns `true` if path is an existing directory. `false` on any error. */
110
+ isDirectory(path: string): boolean;
111
+ /** Returns directory entries (filenames). `[]` on error. */
112
+ readdir(path: string): ReadonlyArray<string>;
113
+ /** Returns file content as utf-8 string. `''` on error. */
114
+ readFile(path: string): string;
115
+ /** Joins path segments per platform separator. */
116
+ join(...parts: string[]): string;
117
+ }
118
+ /**
119
+ * Canonical naming contract for files generated by `*create-epic`.
120
+ *
121
+ * Discovery keeps accepting older forms for compatibility, but new generated
122
+ * files must use only the canonical values below.
123
+ */
124
+ export declare const GENERATED_NAMING_CONTRACT: Readonly<{
125
+ readonly epicId: "P{number}";
126
+ readonly epicFilename: "docs/epics/epic-P{number}.md";
127
+ readonly storyId: "P{number}.NN";
128
+ readonly storyFilename: "docs/stories/P{number}.NN.story.md";
129
+ readonly acceptedOnly: Readonly<{
130
+ epicFilenames: readonly string[];
131
+ storyFilenames: readonly string[];
132
+ }>;
133
+ }>;
134
+ /**
135
+ * Three accepted epic filename conventions.
136
+ * Anchored regex (`^...$`) so partial matches are rejected.
137
+ *
138
+ * - `[0]` generated canonical: `epic-P119.md`
139
+ * accepted legacy: `epic-92.5.md`, `epic-277.md`
140
+ * - `[1]` accepted legacy: `EPIC-01-foundation.md`, `EPIC-12_setup.md`
141
+ * - `[2]` accepted legacy: `epic-customer-portal.md`, `epic-feature-x.md`
142
+ *
143
+ * Group 1 is the canonical ID (or slug) extracted from the filename.
144
+ */
145
+ export declare const EPIC_FILE_PATTERNS: readonly [RegExp, RegExp, RegExp];
146
+ /**
147
+ * Three accepted story filename conventions.
148
+ *
149
+ * - `[0]` generated canonical: `P119.01.story.md`
150
+ * accepted legacy: `01.02.story.md`
151
+ * - `[1]` accepted legacy: `story-P104.04.md`
152
+ * - `[2]` accepted legacy: `EPIC-01.STORY-02.md`
153
+ *
154
+ * Group 1 = story ID for `[0]` and `[1]`. For `[2]`, group 1 = epic id,
155
+ * group 2 = story local id (composite).
156
+ *
157
+ * Pattern[0] accepts both `P119.01.story.md` (P-prefix) and `01.02.story.md`
158
+ * (numeric only) -- the `P?` prefix is optional.
159
+ */
160
+ export declare const STORY_FILE_PATTERNS: readonly [RegExp, RegExp, RegExp];
161
+ /**
162
+ * Default filesystem adapter wrapping `node:fs` synchronous APIs.
163
+ * Lazy-loaded via dynamic require so this module is safe to import in
164
+ * non-Node environments (browser bundles tree-shake unused calls).
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const fs = await buildDefaultDiscoveryFs();
169
+ * const result = discoverEpicsFromDir(dir, fs);
170
+ * ```
171
+ */
172
+ export declare function buildDefaultDiscoveryFs(): Promise<DiscoveryFsAdapter>;
173
+ /**
174
+ * Resolve canonical epic ID from filename + frontmatter.
175
+ *
176
+ * Priority (highest to lowest):
177
+ * 1. Frontmatter `id:` field (operator-authoritative)
178
+ * 2. `EPIC_FILE_PATTERNS[0]` canonical (numeric or P-prefix)
179
+ * 3. `EPIC_FILE_PATTERNS[1]` uppercase numeric
180
+ * 4. `EPIC_FILE_PATTERNS[2]` alpha-slug fallback
181
+ *
182
+ * Returns `null` if no pattern matches AND no frontmatter id.
183
+ *
184
+ * @param filename basename only (e.g., `epic-P119.md`)
185
+ * @param frontmatter parsed frontmatter (may have `id?: string`)
186
+ */
187
+ export declare function getEpicIdFromDescriptor(filename: string, frontmatter: {
188
+ id?: string;
189
+ }): {
190
+ id: string;
191
+ source: EpicIdSource;
192
+ } | null;
193
+ /**
194
+ * Scan `dir` for epic markdown files and return discovered descriptors.
195
+ *
196
+ * Behavior:
197
+ * - Non-recursive scan of `dir/*.md`
198
+ * - Sibling stories directory at `<dir>/../stories/*.story.md` (if exists)
199
+ * - Files with prefix `_` or `.` are skipped (drafts, hidden)
200
+ * - Subdirectories listed in `IGNORED_SUBDIRS` are not recursed
201
+ * - Frontmatter parser extracts `id`, `title`, `status`, `epic_id`
202
+ * - ID derivation: frontmatter > canonical regex > slug fallback
203
+ * - Slug collision: 2nd file with same canonical ID gets `-2`, `-3` suffix + warning
204
+ * - Story-to-epic linking: prefer frontmatter `epic_id` field, fallback filename pattern
205
+ * - Default fs adapter wraps `node:fs` synchronous methods (lazy-loaded)
206
+ *
207
+ * @param dir absolute or relative path to the epics directory
208
+ * @param fs optional fs adapter; defaults to `node:fs` wrapper (lazy)
209
+ * @returns DiscoveryResult with epics, warnings, skipped arrays
210
+ */
211
+ export declare function discoverEpicsFromDir(dir: string, fs: DiscoveryFsAdapter): DiscoveryResult;
212
+ /**
213
+ * Options for `mergeDiscoveredIntoState`.
214
+ *
215
+ * Wire-format public (consumed by P120.04 client hook + tests).
216
+ */
217
+ export interface MergeOptions {
218
+ /**
219
+ * Force discovered entries to overwrite `source: 'create-epic'` records.
220
+ * Default: `false` (defense in depth -- canonical registrations are
221
+ * preserved by default to prevent silent overwrite by misconfigured
222
+ * discovery hooks).
223
+ */
224
+ readonly forceOverwrite?: boolean;
225
+ /**
226
+ * Override `new Date().toISOString()` for deterministic test output.
227
+ * When provided, all `discovered_at` and `merged_at` fields use this value.
228
+ */
229
+ readonly nowIso?: string;
230
+ }
231
+ /**
232
+ * Result of `mergeDiscoveredIntoState`.
233
+ * Caller-readable counters for telemetry / observability.
234
+ */
235
+ export interface MergeResult {
236
+ /** Reference to the (possibly mutated) state object for chaining. */
237
+ readonly state: {
238
+ epics: Record<string, unknown>;
239
+ stories: Record<string, unknown>;
240
+ };
241
+ /** IDs of epics newly added to `state.epics`. */
242
+ readonly added: ReadonlyArray<string>;
243
+ /**
244
+ * IDs of epics skipped by the non-overwrite guard, plus reason.
245
+ * `reason` values: `'create-epic source preserved (defense)'`,
246
+ * `'orphan story (no parent epic in discovered set)'`.
247
+ */
248
+ readonly skipped: ReadonlyArray<{
249
+ id: string;
250
+ reason: string;
251
+ }>;
252
+ /** Number of stories added to `state.stories`. */
253
+ readonly storiesAdded: number;
254
+ }
255
+ /**
256
+ * Merge `DiscoveryResult` (from `discoverEpicsFromDir`) into a `state` object,
257
+ * preserving canonical `*create-epic`-registered entries unless `forceOverwrite`.
258
+ *
259
+ * **Critical defense**: the non-overwrite guard prevents discovered entries
260
+ * from clobbering valuable canonical registrations. Even with `forceOverwrite:
261
+ * true`, the function preserves the existing `source` field for audit trail.
262
+ *
263
+ * **Idempotency**: re-running merge with the same `discovered` + same `state`
264
+ * produces identical output (no duplicate stories, timestamps from first run
265
+ * preserved). Critical for `/loop` iteration which may re-invoke discovery
266
+ * between stories.
267
+ *
268
+ * **Mutation semantics**: state is mutated in place for performance (state.json
269
+ * can have hundreds of entries). Caller (P120.04 client hook) is responsible
270
+ * for deep cloning before merge if needed for diffing/audit.
271
+ *
272
+ * @param state target state object (`null`/`undefined` ok -- treated as empty)
273
+ * @param discovered discovery result (epics + warnings + skipped)
274
+ * @param options optional behavior modifiers
275
+ * @returns `MergeResult` with mutated state + telemetry counters
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const result = mergeDiscoveredIntoState(state, discovered);
280
+ * if (result.skipped.length > 0) {
281
+ * console.warn('skipped:', result.skipped);
282
+ * }
283
+ * console.info(`merged ${result.added.length} epics, ${result.storiesAdded} stories`);
284
+ * ```
285
+ */
286
+ export declare function mergeDiscoveredIntoState(state: {
287
+ epics?: Record<string, unknown>;
288
+ stories?: Record<string, unknown>;
289
+ } | null | undefined, discovered: DiscoveryResult, options?: MergeOptions): MergeResult;
@@ -0,0 +1 @@
1
+ const B=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]),z=Object.freeze([/^(P?\d+\.\d+(?:\.\d+)?)\.story\.md$/i,/^story-(P?\d+\.\d+)\.md$/i,/^EPIC-(\d+)\.STORY-(\d+)\.md$/]),v=Object.freeze(["_archive","_drafts","archive","drafts"]),D=/^---\s*\n([\s\S]*?)\n---/;function O(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 I(t){if(typeof t!="string"||t.length===0)return{};const e=t.match(D);if(!e)return{};const n=e[1];return{id:O(n,"id"),title:O(n,"title"),status:O(n,"status"),epic_id:O(n,"epic_id"),depends_on:R(n),files_to_modify:S(n,"files_to_modify")}}function E(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 R(t){return S(t,"depends_on")}function S(t,e){const n=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=t.match(new RegExp(`^${n}:[ \\t]*(.+?)[ \\t]*$`,"mi"))?.[1]?.trim();if(o!==void 0){const p=(o.startsWith("[")&&o.endsWith("]")?o.slice(1,-1).split(","):[o]).map(E).filter(f=>typeof f=="string"&&f.length>0);return p.length>0?Object.freeze(p):void 0}const m=t.split(/\r?\n/),h=[];let l=!1;for(const _ of m){if(!l){new RegExp(`^${n}:\\s*$`,"i").test(_.trim())&&(l=!0);continue}if(/^[A-Za-z0-9_-]+:\s*/.test(_))break;const p=_.match(/^\s*-\s*(.+?)\s*$/);if(!p)continue;const f=E(p[1]);f&&h.push(f)}return h.length>0?Object.freeze(h):void 0}function F(t){return t.startsWith("_")||t.startsWith(".")}function N(t){return v.includes(t)}async function U(){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 x(t,e){let n=!1,s=null,o=null;for(let m=0;m<$.length;m++){const h=$[m],l=t.match(h);if(l){n=!0,s=l[1],o=m===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 T(t){for(const e of $){const n=t.match(e);if(n)return n[1]}return null}function j(t){const e=t.trim();return/^\d+(?:\.\d+)*$/i.test(e)?`P${e}`.toUpperCase():e.toUpperCase()}function W(t){const e=j(t);return/^P\d+(?:\.\d+)?$/i.test(e)?`epic-${e}.md`:`epic-${t}.md`}function C(t){const e=j(t);return/^P\d+\.\d+(?:\.\d+)?$/i.test(e)?`${e}.story.md`:`${t}.story.md`}function w(t,e){return!t||!e?!1:j(t)!==j(e)}function M(t,e,n){return`frontmatter/filename mismatch: epic '${t}' has frontmatter id '${e}' but filename implies '${n}'. Repair by using generated canonical '${W(e)}' or changing frontmatter id to '${n}'.`}function k(t,e,n){return`frontmatter/filename mismatch: story '${t}' has frontmatter id '${e}' but filename implies '${n}'. Repair by using generated canonical '${C(e)}' or changing frontmatter id to '${n}'.`}function A(t){let e=t.match(z[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(z[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(z[2]),e){const n=e[1],s=e[2];return{id:`${n}.${s}`,epic_id_hint:n}}return null}function L(t,e){return e.join(t,"..","stories")}function Y(t,e){const n=[],s=[],o=[],m=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 h=[...e.readdir(t)].sort(),l=[];for(const i of h){const r=e.join(t,i);if(F(i)){s.push(`${i}: ignored (draft or hidden prefix)`);continue}if(e.isDirectory(r)){if(N(i)){s.push(`${i}: ignored subdirectory`);continue}s.push(`${i}: subdirectory not recursed`);continue}if(!i.endsWith(".md"))continue;if(!$.some(g=>g.test(i))){s.push(`${i}: did not match any EPIC_FILE_PATTERNS`);continue}const d=e.readFile(r),y=I(d),u=T(i);y.id&&w(y.id,u)&&n.push(M(i,y.id,u)),l.push({filename:i,fullPath:r,frontmatter:y})}const _=new Map,p=[];for(const i of l){const r=x(i.filename,i.frontmatter);if(r===null){s.push(`${i.filename}: id resolution returned null`);continue}let c=r.id;const d=c,y=_.get(d)??0;y>0&&(c=`${d}-${y+1}`,n.push(`epic ID collision: '${d}' resolved to '${c}' for ${i.filename}`)),_.set(d,y+1),p.push({candidate:i,finalId:c,idSource:r.source})}const f=new Map,b=L(t,e);if(e.isDirectory(b)){const i=[...e.readdir(b)].sort();for(const r of i){if(F(r)||!r.endsWith(".md"))continue;const c=e.join(b,r);if(e.isDirectory(c))continue;const d=A(r);if(!d)continue;const y=e.readFile(c),u=I(y);u.id&&w(u.id,d.id)&&n.push(k(r,u.id,d.id));const g=u.epic_id??d.epic_id_hint;if(!g){n.push(`story '${r}' has no epic_id (frontmatter or filename pattern)`);continue}const P=Object.freeze({id:d.id,epic_id:g,filename:r,path:c,...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}:{}});f.has(g)||f.set(g,[]),f.get(g).push(P)}}for(const i of p){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:m,stories:Object.freeze([...r]),e:i.idSource});o.push(c)}const a=new Set(o.map(i=>i.id));for(const[i,r]of f.entries())if(!a.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 G(t){if(typeof t!="string")return!1;const e=t.trim().toLowerCase().replace(/_/g,"-");return e==="done"||e==="completed"||e==="merged"}function Z(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,m=s.stories,h=[],l=[];let _=0;const p=n?.forceOverwrite===!0,f=n?.nowIso??new Date().toISOString(),b=new Set;for(const a of e.epics)b.add(a.id);for(const a of e.epics){const i=o[a.id];if(i&&i.source==="create-epic"&&!p){l.push({id:a.id,reason:"create-epic source preserved (defense)"});continue}if(i&&i.source==="discovered"&&!p){const c=i;c.depends_on=a.depends_on??[];continue}const r={id:a.id,title:a.title??"",status:a.status??"planning",source:"discovered",discovered_at:f,merged_at:f,stories:a.stories.map(c=>c.id),depends_on:a.depends_on??[]};o[a.id]=r,h.push(a.id)}for(const a of e.epics)for(const i of a.stories){const r=m[i.id];if(r&&r.source==="create-epic"&&!p){l.push({id:i.id,reason:"create-epic source preserved (defense, story)"});continue}if(r&&G(r.status)&&!p){const d=r;d.depends_on=i.depends_on??[],d.files_to_modify=i.files_to_modify??[];continue}if(r&&r.source==="discovered"&&!p){const d=r;d.depends_on=i.depends_on??[],d.files_to_modify=i.files_to_modify??[];continue}const c={id:i.id,epic_id:i.epic_id,title:i.title??"",status:i.status??"backlog",source:"discovered",discovered_at:f,depends_on:i.depends_on??[],files_to_modify:i.files_to_modify??[]};m[i.id]=c,_++}return{state:s,added:Object.freeze([...h]),skipped:Object.freeze([...l]),storiesAdded:_}}export{$ as EPIC_FILE_PATTERNS,B as GENERATED_NAMING_CONTRACT,z as STORY_FILE_PATTERNS,U as buildDefaultDiscoveryFs,Y as discoverEpicsFromDir,x as getEpicIdFromDescriptor,Z as mergeDiscoveredIntoState};