@ornexus/neocortex 4.60.68 → 4.60.69

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 (232) hide show
  1. package/dist/release-source.json +1 -1
  2. package/dist/sbom.cdx.json +1194 -1194
  3. package/install.ps1 +1 -1
  4. package/install.sh +1 -1
  5. package/package.json +2 -2
  6. package/packages/client/dist/adapters/adapter-registry.js +1 -108
  7. package/packages/client/dist/adapters/antigravity-adapter.js +2 -77
  8. package/packages/client/dist/adapters/claude-code-adapter.js +3 -79
  9. package/packages/client/dist/adapters/codex-adapter.js +2 -80
  10. package/packages/client/dist/adapters/cursor-adapter.js +4 -115
  11. package/packages/client/dist/adapters/gemini-adapter.js +2 -71
  12. package/packages/client/dist/adapters/grok-adapter.js +2 -42
  13. package/packages/client/dist/adapters/index.js +1 -22
  14. package/packages/client/dist/adapters/platform-detector.js +1 -212
  15. package/packages/client/dist/adapters/target-adapter.js +0 -12
  16. package/packages/client/dist/adapters/vscode-adapter.js +2 -72
  17. package/packages/client/dist/agent/refresh-stubs.js +2 -417
  18. package/packages/client/dist/agent/update-agent-yaml.js +1 -102
  19. package/packages/client/dist/agent/update-description.js +1 -251
  20. package/packages/client/dist/cache/crypto-utils.js +1 -78
  21. package/packages/client/dist/cache/encrypted-cache.js +1 -108
  22. package/packages/client/dist/cache/in-memory-asset-cache.js +1 -73
  23. package/packages/client/dist/cache/index.js +1 -13
  24. package/packages/client/dist/cache/protected-pi-boundary.js +1 -44
  25. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -127
  26. package/packages/client/dist/checkpoint/index.js +1 -11
  27. package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -32
  28. package/packages/client/dist/cli.js +3 -290
  29. package/packages/client/dist/commands/activate.js +8 -394
  30. package/packages/client/dist/commands/architecture-answers.js +7 -274
  31. package/packages/client/dist/commands/architecture-memory-terminal.js +4 -253
  32. package/packages/client/dist/commands/architecture-non-ui-continuation.js +1 -100
  33. package/packages/client/dist/commands/architecture-root-binding.js +1 -51
  34. package/packages/client/dist/commands/cache-status.js +2 -112
  35. package/packages/client/dist/commands/config.js +1 -88
  36. package/packages/client/dist/commands/doctor.js +2 -635
  37. package/packages/client/dist/commands/invoke.d.ts +6 -0
  38. package/packages/client/dist/commands/invoke.js +142 -7140
  39. package/packages/client/dist/commands/legacy-epic-terminal.js +1 -71
  40. package/packages/client/dist/commands/legacy-story-branch.js +1 -96
  41. package/packages/client/dist/commands/legacy-story-pr.js +1 -276
  42. package/packages/client/dist/commands/mutation-qa-checkpoint.js +5 -285
  43. package/packages/client/dist/commands/mutation-qa-gate.js +1 -54
  44. package/packages/client/dist/commands/mutation-qa-git.js +1 -41
  45. package/packages/client/dist/commands/mutation-qa-policy.js +1 -102
  46. package/packages/client/dist/commands/mutation-qa-run.js +3 -219
  47. package/packages/client/dist/commands/mutation-qa-verdict.js +1 -149
  48. package/packages/client/dist/commands/mutation-qa.js +1 -220
  49. package/packages/client/dist/commands/native-agent.js +2 -136
  50. package/packages/client/dist/commands/planning-diagnosis.js +1 -43
  51. package/packages/client/dist/commands/planning-generation.js +1 -53
  52. package/packages/client/dist/commands/planning-memory.js +1 -40
  53. package/packages/client/dist/commands/planning-research.js +1 -42
  54. package/packages/client/dist/commands/private-session.js +1 -75
  55. package/packages/client/dist/commands/property-qa.js +1 -139
  56. package/packages/client/dist/commands/qa-control.js +1 -16
  57. package/packages/client/dist/commands/qa-scan-browser-launch.js +1 -55
  58. package/packages/client/dist/commands/qa-scan-browser-route.js +1 -28
  59. package/packages/client/dist/commands/qa-scan-capture.js +5 -663
  60. package/packages/client/dist/commands/qa-scan-discovery.js +3 -376
  61. package/packages/client/dist/commands/qa-scan-review.js +5 -826
  62. package/packages/client/dist/commands/qa-synthetic-backend-capture.js +1 -266
  63. package/packages/client/dist/commands/qa-synthetic-backend-review.js +1 -297
  64. package/packages/client/dist/commands/qa-synthetic-backend.js +2 -340
  65. package/packages/client/dist/commands/qa-synthetic-broker-port.js +1 -85
  66. package/packages/client/dist/commands/qa-synthetic-browser.js +1 -39
  67. package/packages/client/dist/commands/refactor-characterization.js +21 -671
  68. package/packages/client/dist/commands/refactor-commit.js +13 -399
  69. package/packages/client/dist/commands/refactor-control.js +3 -142
  70. package/packages/client/dist/commands/refactor-cpp-assertions.js +3 -93
  71. package/packages/client/dist/commands/refactor-review.js +5 -283
  72. package/packages/client/dist/commands/refactor-root.js +7 -588
  73. package/packages/client/dist/commands/refresh-memory.js +3 -156
  74. package/packages/client/dist/commands/shared-architecture-non-ui.js +1 -24
  75. package/packages/client/dist/commands/story-checkpoint.js +1 -171
  76. package/packages/client/dist/config/profile-directory.js +1 -13
  77. package/packages/client/dist/config/resolver-selection.js +1 -278
  78. package/packages/client/dist/config/secure-config.js +12 -281
  79. package/packages/client/dist/config.js +2 -223
  80. package/packages/client/dist/constants.js +1 -25
  81. package/packages/client/dist/context/context-collector.js +2 -222
  82. package/packages/client/dist/context/context-sanitizer.js +1 -145
  83. package/packages/client/dist/continuity/autonomous-governance.js +1 -285
  84. package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1587
  85. package/packages/client/dist/continuity/invoke-hooks.js +1 -129
  86. package/packages/client/dist/continuity/migrations/001-initial-schema.js +2 -12
  87. package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +2 -12
  88. package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +2 -12
  89. package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +2 -12
  90. package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +2 -12
  91. package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +2 -10
  92. package/packages/client/dist/continuity/native-agent-store.js +1 -69
  93. package/packages/client/dist/continuity/native-agent-types.js +1 -90
  94. package/packages/client/dist/continuity/outbox.js +1 -117
  95. package/packages/client/dist/continuity/runner-bootstrap-policy.js +1 -172
  96. package/packages/client/dist/continuity/sqlite-store.js +81 -2536
  97. package/packages/client/dist/effects/invoke-effect-gateways.js +1 -62
  98. package/packages/client/dist/errors/error-messages.js +2 -228
  99. package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -125
  100. package/packages/client/dist/evidence/collector.js +1 -347
  101. package/packages/client/dist/evidence/index.js +1 -1
  102. package/packages/client/dist/evidence/types.js +0 -2
  103. package/packages/client/dist/fs/atomic-state-replace.js +1 -152
  104. package/packages/client/dist/generated/command-registry.generated.js +1 -4076
  105. package/packages/client/dist/generated/public-projections.generated.js +1 -149
  106. package/packages/client/dist/git/direct-yolo-c01-retry.js +4 -138
  107. package/packages/client/dist/git/direct-yolo-checkpoint.js +4 -1234
  108. package/packages/client/dist/git/git-lifecycle-step-ids.js +1 -14
  109. package/packages/client/dist/git/git-lifecycle-store.js +9 -1938
  110. package/packages/client/dist/git/legacy-story-branch-checkpoint.js +9 -259
  111. package/packages/client/dist/git/legacy-story-branch-setup.js +4 -348
  112. package/packages/client/dist/git/legacy-story-pr.js +70 -3538
  113. package/packages/client/dist/git/secure-filesystem-boundary.js +5 -182
  114. package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -284
  115. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -237
  116. package/packages/client/dist/i18n/first-run.js +2 -60
  117. package/packages/client/dist/index.js +1 -47
  118. package/packages/client/dist/invocation/invoke-input.js +1 -24
  119. package/packages/client/dist/license/index.js +1 -5
  120. package/packages/client/dist/license/license-client.js +1 -304
  121. package/packages/client/dist/machine/fingerprint.js +2 -163
  122. package/packages/client/dist/machine/index.js +1 -5
  123. package/packages/client/dist/memory/project-memory-path-scope.js +1 -62
  124. package/packages/client/dist/memory/project-memory-writer.js +40 -1007
  125. package/packages/client/dist/memory/shared-project-memory-types.js +2 -253
  126. package/packages/client/dist/migrations/project-identity-capability.js +1 -50
  127. package/packages/client/dist/migrations/project-identity.js +7 -493
  128. package/packages/client/dist/migrations/state-json.js +4 -344
  129. package/packages/client/dist/policy/architecture-policy.js +2 -184
  130. package/packages/client/dist/policy/index.js +1 -7
  131. package/packages/client/dist/policy/shared-policy-types.js +0 -38
  132. package/packages/client/dist/policy/subagent-approval-policy.js +1 -55
  133. package/packages/client/dist/private-execution/clean-baseline.js +2 -279
  134. package/packages/client/dist/private-execution/codex-executor.js +4 -574
  135. package/packages/client/dist/private-execution/codex-host-runtime.js +1 -137
  136. package/packages/client/dist/private-execution/codex-lifecycle-guard.js +1 -97
  137. package/packages/client/dist/private-execution/codex-scoped-permissions.js +1 -128
  138. package/packages/client/dist/private-execution/delivery.js +13 -410
  139. package/packages/client/dist/private-execution/diagnosis-root-review.js +2 -294
  140. package/packages/client/dist/private-execution/frozen-handoff.js +6 -19
  141. package/packages/client/dist/private-execution/implementation-validation.js +2 -106
  142. package/packages/client/dist/private-execution/isolated-admission.js +23 -1432
  143. package/packages/client/dist/private-execution/isolated-session.js +5 -725
  144. package/packages/client/dist/private-execution/isolated-workspace.js +1 -122
  145. package/packages/client/dist/private-execution/pact-consumer-runtime.js +3 -409
  146. package/packages/client/dist/private-execution/pact-test-source.js +1 -258
  147. package/packages/client/dist/private-execution/pact-toolchain.js +3 -290
  148. package/packages/client/dist/private-execution/pact-write-set.js +1 -172
  149. package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +5 -406
  150. package/packages/client/dist/private-execution/planning-generation-checkpoint.js +5 -462
  151. package/packages/client/dist/private-execution/planning-memory-checkpoint.js +3 -139
  152. package/packages/client/dist/private-execution/planning-research-checkpoint.js +4 -325
  153. package/packages/client/dist/private-execution/private-report-store.js +1 -42
  154. package/packages/client/dist/private-execution/property-test-candidate.js +1 -103
  155. package/packages/client/dist/private-execution/property-test-choices.js +3 -147
  156. package/packages/client/dist/private-execution/property-test-plan.js +7 -271
  157. package/packages/client/dist/private-execution/property-test-review.js +4 -258
  158. package/packages/client/dist/private-execution/property-test-runtime.js +9 -372
  159. package/packages/client/dist/private-execution/property-test-source.js +1 -139
  160. package/packages/client/dist/private-execution/root-checkpoint.js +1 -80
  161. package/packages/client/dist/private-execution/root-memory-git-evidence.js +3 -135
  162. package/packages/client/dist/private-execution/root-memory-recovery.js +1 -93
  163. package/packages/client/dist/private-execution/root-memory-request.js +1 -29
  164. package/packages/client/dist/private-execution/root-memory-scope.js +1 -178
  165. package/packages/client/dist/private-execution/root-memory-writer.js +1 -102
  166. package/packages/client/dist/private-execution/session-lease.js +1 -63
  167. package/packages/client/dist/private-execution/shared-private-admission-types.js +1 -50
  168. package/packages/client/dist/private-execution/shared-project-validation.js +1 -66
  169. package/packages/client/dist/private-execution/shared-root-memory-types.js +1 -61
  170. package/packages/client/dist/private-execution/ux-source-validation.js +1 -372
  171. package/packages/client/dist/private-execution/ux-write-set.js +2 -191
  172. package/packages/client/dist/resilience/circuit-breaker.js +1 -170
  173. package/packages/client/dist/resilience/degradation-manager.js +1 -164
  174. package/packages/client/dist/resilience/freshness-indicator.js +1 -100
  175. package/packages/client/dist/resilience/index.js +1 -8
  176. package/packages/client/dist/resilience/recovery-detector.js +1 -74
  177. package/packages/client/dist/resolvers/asset-resolver.js +0 -13
  178. package/packages/client/dist/resolvers/local-resolver.js +8 -255
  179. package/packages/client/dist/resolvers/remote-resolver.js +1 -233
  180. package/packages/client/dist/runner/binary.js +1 -404
  181. package/packages/client/dist/runner/cli.js +17 -1571
  182. package/packages/client/dist/runner/headless/adapters.js +1 -49
  183. package/packages/client/dist/runner/headless/claude-code-adapter.js +1 -79
  184. package/packages/client/dist/runner/headless/continuous-story-workspace.js +11 -1628
  185. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -69
  186. package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +2 -110
  187. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -1236
  188. package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +2 -318
  189. package/packages/client/dist/runner/headless/fake-adapter.js +1 -62
  190. package/packages/client/dist/runner/headless/managed-profile.js +1 -132
  191. package/packages/client/dist/runner/headless/opencode-adapter.js +2 -145
  192. package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -43
  193. package/packages/client/dist/runner/headless/resolver.js +1 -231
  194. package/packages/client/dist/runner/headless/run-yolo.js +1 -885
  195. package/packages/client/dist/runner/headless/run-yoloop.js +2 -2574
  196. package/packages/client/dist/runner/headless/types.js +2 -574
  197. package/packages/client/dist/runner/local-console.js +13 -396
  198. package/packages/client/dist/runner/portal-sync-worker.js +1 -1487
  199. package/packages/client/dist/runner/project-identity.js +1 -106
  200. package/packages/client/dist/runner/scheduler.js +7 -995
  201. package/packages/client/dist/runner-cli.js +2 -18
  202. package/packages/client/dist/state/invocation-shape.js +1 -13
  203. package/packages/client/dist/state/project-state-snapshot.js +1 -1677
  204. package/packages/client/dist/state/state-json-repair.js +3 -785
  205. package/packages/client/dist/state/structural-dag-projection.js +1 -118
  206. package/packages/client/dist/telemetry/index.js +1 -5
  207. package/packages/client/dist/telemetry/offline-queue.js +1 -131
  208. package/packages/client/dist/tier/index.js +1 -5
  209. package/packages/client/dist/tier/tier-aware-client.js +1 -405
  210. package/packages/client/dist/types/index.js +1 -38
  211. package/packages/client/dist/version.js +1 -27
  212. package/packages/client/dist/yolo/change-evidence-collector.js +1 -126
  213. package/packages/client/dist/yolo/invoke-envelope-capability.js +1 -62
  214. package/packages/client/dist/yolo/spec-envelope-collector.js +5 -185
  215. package/packages/client/dist/yoloop/discovery-hook.js +2 -300
  216. package/packages/client/dist/yoloop/index.js +1 -8
  217. package/packages/client/dist/yoloop/invoke-hooks.js +12 -669
  218. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1007
  219. package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -33
  220. package/packages/client/dist/yoloop/target-normalization.js +1 -180
  221. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1723
  222. package/targets-stubs/antigravity/gemini.md +1 -1
  223. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  224. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  225. package/targets-stubs/claude-code/neocortex-root.md +2 -2
  226. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  227. package/targets-stubs/claude-code/neocortex.md +2 -2
  228. package/targets-stubs/codex/AGENTS.md +1 -1
  229. package/targets-stubs/cursor/agent.md +2 -2
  230. package/targets-stubs/gemini-cli/agent.md +2 -2
  231. package/targets-stubs/opencode/neocortex-root.md +1 -1
  232. package/targets-stubs/vscode/neocortex.agent.md +2 -2
@@ -1,62 +1 @@
1
- import { YOLO_INVOKE_ENVELOPE_CAPABILITY } from '../../../shared/dist/yolo/spec-envelope.js';
2
- const MAX_CAPABILITY_RESPONSE_BYTES = 16 * 1024;
3
- const PUBLIC_VERSION = /^[0-9]{1,6}\.[0-9]{1,6}\.[0-9]{1,6}(?:-[A-Za-z0-9.-]{1,40})?$/;
4
- function result(supported, reason, serverVersion) {
5
- return Object.freeze({
6
- supported,
7
- capability: YOLO_INVOKE_ENVELOPE_CAPABILITY,
8
- reason,
9
- ...(serverVersion && PUBLIC_VERSION.test(serverVersion) ? { serverVersion } : {}),
10
- publicSafe: true,
11
- });
12
- }
13
- /**
14
- * Non-mutating, fail-closed compatibility probe for targeted YOLO requests.
15
- * Older strict servers must be rejected before local release-gate execution.
16
- */
17
- export async function probeYoloInvokeEnvelopeCapability(serverUrl, fetcher = globalThis.fetch) {
18
- if (!fetcher)
19
- return result(false, 'fetch-unavailable');
20
- const controller = new AbortController();
21
- const timeout = setTimeout(() => controller.abort(), 5_000);
22
- timeout.unref?.();
23
- try {
24
- const response = await fetcher(`${serverUrl.replace(/\/+$/, '')}/api/v1/health`, {
25
- method: 'GET',
26
- headers: { accept: 'application/json' },
27
- signal: controller.signal,
28
- });
29
- if (!response.ok)
30
- return result(false, 'http-rejected');
31
- const raw = await response.text();
32
- if (Buffer.byteLength(raw, 'utf8') > MAX_CAPABILITY_RESPONSE_BYTES) {
33
- return result(false, 'response-too-large');
34
- }
35
- let parsed;
36
- try {
37
- parsed = JSON.parse(raw);
38
- }
39
- catch {
40
- return result(false, 'response-invalid');
41
- }
42
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
43
- return result(false, 'response-invalid');
44
- }
45
- const record = parsed;
46
- const capabilities = record.capabilities;
47
- const serverVersion = typeof record.version === 'string' ? record.version : undefined;
48
- if (!Array.isArray(capabilities)
49
- || capabilities.some((value) => typeof value !== 'string' || value.length > 80)) {
50
- return result(false, 'response-invalid', serverVersion);
51
- }
52
- return capabilities.includes(YOLO_INVOKE_ENVELOPE_CAPABILITY)
53
- ? result(true, 'supported', serverVersion)
54
- : result(false, 'capability-missing', serverVersion);
55
- }
56
- catch {
57
- return result(false, 'request-failed');
58
- }
59
- finally {
60
- clearTimeout(timeout);
61
- }
62
- }
1
+ import{YOLO_INVOKE_ENVELOPE_CAPABILITY as u}from"../../../shared/dist/yolo/spec-envelope.js";const y=16*1024,b=/^[0-9]{1,6}\.[0-9]{1,6}\.[0-9]{1,6}(?:-[A-Za-z0-9.-]{1,40})?$/;function e(n,a,t){return Object.freeze({supported:n,capability:u,reason:a,...t&&b.test(t)?{serverVersion:t}:{},publicSafe:!0})}async function h(n,a=globalThis.fetch){if(!a)return e(!1,"fetch-unavailable");const t=new AbortController,l=setTimeout(()=>t.abort(),5e3);l.unref?.();try{const c=await a(`${n.replace(/\/+$/,"")}/api/v1/health`,{method:"GET",headers:{accept:"application/json"},signal:t.signal});if(!c.ok)return e(!1,"http-rejected");const f=await c.text();if(Buffer.byteLength(f,"utf8")>y)return e(!1,"response-too-large");let r;try{r=JSON.parse(f)}catch{return e(!1,"response-invalid")}if(!r||typeof r!="object"||Array.isArray(r))return e(!1,"response-invalid");const i=r,s=i.capabilities,o=typeof i.version=="string"?i.version:void 0;return!Array.isArray(s)||s.some(p=>typeof p!="string"||p.length>80)?e(!1,"response-invalid",o):s.includes(u)?e(!0,"supported",o):e(!1,"capability-missing",o)}catch{return e(!1,"request-failed")}finally{clearTimeout(l)}}export{h as probeYoloInvokeEnvelopeCapability};
@@ -1,185 +1,5 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
- import { join, sep } from 'node:path';
4
- import { createYoloSpecEnvelopeV1, YOLO_SPEC_ARTIFACT_LIMIT_BYTES, YOLO_SPEC_MAX_ARTIFACTS, YOLO_SPEC_TOTAL_LIMIT_BYTES, } from '../../../shared/dist/yolo/spec-envelope.js';
5
- import { inspectFrontmatterContext } from '../../../shared/dist/frontmatter-context-contract.js';
6
- const TEMPLATE_PATHS = [
7
- 'core/templates/specs/feature-spec-template.md',
8
- 'core/templates/specs/architecture-spec-template.md',
9
- 'core/templates/specs/contract-spec-template.md',
10
- 'core/templates/specs/story-spec-template.md',
11
- 'core/templates/specs/traceability-matrix-template.md',
12
- ];
13
- const REFERENCE_PATH = /(?:^|[\s([`'";])((?:docs\/(?:stories|epics|specs|architecture|examples)\/|core\/templates\/specs\/)[A-Za-z0-9_./-]+\.(?:md|mdx|feature))(?:$|[\s)\]`'",;])/gm;
14
- const STORY_ID = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+$/;
15
- /** Resolve only an explicit story target. Inline YOLO text intentionally has no match. */
16
- export function extractTargetedYoloStoryId(args) {
17
- const pathMatches = [...args.matchAll(/(?:^|\s)@?docs\/stories\/([A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+)\.story\.md(?=\s|$)/g)]
18
- .map((match) => match[1]);
19
- const tokenMatches = args.trim().split(/\s+/).slice(1)
20
- .map((token) => token.replace(/^@/, ''))
21
- .filter((token) => STORY_ID.test(token));
22
- const matches = [...new Set([...pathMatches, ...tokenMatches])];
23
- return matches.length === 1 ? matches[0] : null;
24
- }
25
- function artifactKind(path) {
26
- if (path.startsWith('core/templates/'))
27
- return 'template';
28
- if (path.startsWith('docs/epics/'))
29
- return 'epic';
30
- if (path.startsWith('docs/stories/') && path.endsWith('.story.md'))
31
- return 'story';
32
- if (path.endsWith('.rtm.md'))
33
- return 'traceability';
34
- if (path.toLowerCase().includes('adr'))
35
- return 'adr';
36
- if (path.toLowerCase().includes('contract'))
37
- return 'contract';
38
- if (path.toLowerCase().includes('nfr'))
39
- return 'nfr';
40
- if (path.endsWith('.feature'))
41
- return 'bdd';
42
- if (path.toLowerCase().includes('traceability'))
43
- return 'traceability';
44
- if (path.startsWith('docs/examples/'))
45
- return 'example';
46
- return 'spec';
47
- }
48
- function selectedReferencePaths(story) {
49
- return [...new Set([...story.matchAll(REFERENCE_PATH)].map((match) => match[1]).filter((path) => Boolean(path)))].sort();
50
- }
51
- function safeSource(root, path) {
52
- const rootReal = realpathSync(root);
53
- let fileReal;
54
- try {
55
- fileReal = realpathSync(join(rootReal, path));
56
- }
57
- catch {
58
- throw new Error('yolo-spec-envelope-required');
59
- }
60
- if (!fileReal.startsWith(`${rootReal}${sep}`))
61
- throw new Error('yolo-spec-envelope-path-out-of-scope');
62
- if (!statSync(fileReal).isFile())
63
- throw new Error('yolo-spec-envelope-unsafe');
64
- if (statSync(fileReal).size > YOLO_SPEC_ARTIFACT_LIMIT_BYTES)
65
- throw new Error('yolo-spec-envelope-too-large');
66
- return readFileSync(fileReal, 'utf8');
67
- }
68
- function publicMarkdown(source, kind) {
69
- const context = kind === 'story' ? 'story-target' : 'epic-target';
70
- const inspected = inspectFrontmatterContext(source, { context, explicitlySelected: true });
71
- if (inspected.status === 'malformed' || inspected.status === 'oversized')
72
- throw new Error('yolo-spec-envelope-unsafe');
73
- if (inspected.status === 'absent')
74
- return source;
75
- const match = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
76
- if (!match)
77
- throw new Error('yolo-spec-envelope-unsafe');
78
- const fields = Object.entries(inspected.metadata).map(([key, value]) => `${key}: ${JSON.stringify(value)}`);
79
- return `---\n${fields.join('\n')}\n---\n${source.slice(match[0].length)}`;
80
- }
81
- function readHeadSha(root) {
82
- try {
83
- return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], {
84
- encoding: 'utf8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'],
85
- }).trim();
86
- }
87
- catch {
88
- throw new Error('yolo-spec-envelope-stale');
89
- }
90
- }
91
- export function collectYoloSpecEnvelope(options) {
92
- const targetPath = `docs/stories/${options.storyId}.story.md`;
93
- const epicPath = `docs/epics/epic-${options.epicId}.md`;
94
- const initialHead = options.headSha ?? readHeadSha(options.projectRoot);
95
- const story = publicMarkdown(safeSource(options.projectRoot, targetPath), 'story');
96
- const epic = publicMarkdown(safeSource(options.projectRoot, epicPath), 'epic');
97
- const artifacts = [
98
- { kind: 'story', path: targetPath, content: story },
99
- { kind: 'epic', path: epicPath, content: epic },
100
- ];
101
- const coreStoryReferences = new Set([
102
- `docs/stories/${options.storyId}.spec.md`,
103
- `docs/stories/${options.storyId}.tasks.md`,
104
- `docs/stories/${options.storyId}.rtm.md`,
105
- ]);
106
- // Older stories may not link to their own SDD files. Bind physically present
107
- // siblings to the same HEAD/content checks as explicitly referenced files.
108
- const selectedReferences = [...new Set([
109
- ...selectedReferencePaths(story),
110
- ...[...coreStoryReferences].filter((path) => existsSync(join(options.projectRoot, path))),
111
- ])].sort();
112
- const prioritizedReferences = [
113
- ...selectedReferences.filter((path) => coreStoryReferences.has(path)),
114
- ...selectedReferences.filter((path) => !coreStoryReferences.has(path)),
115
- ];
116
- let selectedBytes = artifacts.reduce((sum, artifact) => sum + Buffer.byteLength(artifact.content, 'utf8'), 0);
117
- let omittedReferences = 0;
118
- for (const path of prioritizedReferences) {
119
- if (path === targetPath || path === epicPath || !existsSync(join(options.projectRoot, path)))
120
- continue;
121
- // A foreign story/epic is a target conflict, never optional context. The
122
- // byte budget must not turn a prior target-mismatch denial into admission.
123
- if ((path.startsWith('docs/stories/') && path.endsWith('.story.md'))
124
- || path.startsWith('docs/epics/'))
125
- throw new Error('yolo-spec-envelope-target-mismatch');
126
- const content = safeSource(options.projectRoot, path);
127
- const bytes = Buffer.byteLength(content, 'utf8');
128
- if (artifacts.length >= YOLO_SPEC_MAX_ARTIFACTS || selectedBytes + bytes > YOLO_SPEC_TOTAL_LIMIT_BYTES) {
129
- if (coreStoryReferences.has(path))
130
- throw new Error('yolo-spec-envelope-too-large');
131
- omittedReferences += 1;
132
- continue;
133
- }
134
- artifacts.push({ kind: artifactKind(path), path, content });
135
- selectedBytes += bytes;
136
- }
137
- // Templates and non-core references are supporting context. Preserve the
138
- // story's own SDD artifacts and signal omissions without exposing paths.
139
- if (omittedReferences)
140
- options.onOmittedReferences?.(omittedReferences);
141
- for (const path of TEMPLATE_PATHS) {
142
- if (path === targetPath || path === epicPath || selectedReferences.includes(path)
143
- || !existsSync(join(options.projectRoot, path)))
144
- continue;
145
- const content = safeSource(options.projectRoot, path);
146
- const bytes = Buffer.byteLength(content, 'utf8');
147
- if (artifacts.length >= YOLO_SPEC_MAX_ARTIFACTS || selectedBytes + bytes > YOLO_SPEC_TOTAL_LIMIT_BYTES)
148
- continue;
149
- artifacts.push({ kind: artifactKind(path), path, content });
150
- selectedBytes += bytes;
151
- }
152
- const envelope = createYoloSpecEnvelopeV1({
153
- storyId: options.storyId,
154
- epicId: options.epicId,
155
- targetPath,
156
- inputSha256: options.inputSha256,
157
- headSha: initialHead,
158
- collectedAt: options.collectedAt ?? new Date().toISOString(),
159
- artifacts,
160
- });
161
- const finalHead = options.headSha ?? readHeadSha(options.projectRoot);
162
- if (initialHead !== finalHead
163
- || publicMarkdown(safeSource(options.projectRoot, targetPath), 'story') !== story
164
- || publicMarkdown(safeSource(options.projectRoot, epicPath), 'epic') !== epic
165
- || artifacts.some((artifact) => artifact.kind !== 'story' && artifact.kind !== 'epic'
166
- && safeSource(options.projectRoot, artifact.path) !== artifact.content)) {
167
- throw new Error('yolo-spec-envelope-stale');
168
- }
169
- return envelope;
170
- }
171
- /**
172
- * Reissue a precollected physical snapshot with a fresh bounded transport time.
173
- * The caller still performs the final disk/HEAD collection and digest comparison.
174
- */
175
- export function refreshPrecollectedYoloSpecEnvelope(envelope, collectedAt = new Date().toISOString()) {
176
- return createYoloSpecEnvelopeV1({
177
- storyId: envelope.storyId,
178
- epicId: envelope.epicId,
179
- targetPath: envelope.targetPath,
180
- inputSha256: envelope.inputSha256,
181
- headSha: envelope.sourceRevision.headSha,
182
- collectedAt,
183
- artifacts: envelope.artifacts.map(({ kind, path, content }) => ({ kind, path, content })),
184
- });
185
- }
1
+ import{execFileSync as L}from"node:child_process";import{existsSync as S,readFileSync as $,realpathSync as w,statSync as R}from"node:fs";import{join as f,sep as b}from"node:path";import{createYoloSpecEnvelopeV1 as E,YOLO_SPEC_ARTIFACT_LIMIT_BYTES as O,YOLO_SPEC_MAX_ARTIFACTS as g,YOLO_SPEC_TOTAL_LIMIT_BYTES as I}from"../../../shared/dist/yolo/spec-envelope.js";import{inspectFrontmatterContext as v}from"../../../shared/dist/frontmatter-context-contract.js";const C=["core/templates/specs/feature-spec-template.md","core/templates/specs/architecture-spec-template.md","core/templates/specs/contract-spec-template.md","core/templates/specs/story-spec-template.md","core/templates/specs/traceability-matrix-template.md"],P=/(?:^|[\s([`'";])((?:docs\/(?:stories|epics|specs|architecture|examples)\/|core\/templates\/specs\/)[A-Za-z0-9_./-]+\.(?:md|mdx|feature))(?:$|[\s)\]`'",;])/gm,W=/^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+$/;function Z(e){const r=[...e.matchAll(/(?:^|\s)@?docs\/stories\/([A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+)\.story\.md(?=\s|$)/g)].map(c=>c[1]),s=e.trim().split(/\s+/).slice(1).map(c=>c.replace(/^@/,"")).filter(c=>W.test(c)),o=[...new Set([...r,...s])];return o.length===1?o[0]:null}function A(e){return e.startsWith("core/templates/")?"template":e.startsWith("docs/epics/")?"epic":e.startsWith("docs/stories/")&&e.endsWith(".story.md")?"story":e.endsWith(".rtm.md")?"traceability":e.toLowerCase().includes("adr")?"adr":e.toLowerCase().includes("contract")?"contract":e.toLowerCase().includes("nfr")?"nfr":e.endsWith(".feature")?"bdd":e.toLowerCase().includes("traceability")?"traceability":e.startsWith("docs/examples/")?"example":"spec"}function z(e){return[...new Set([...e.matchAll(P)].map(r=>r[1]).filter(r=>!!r))].sort()}function a(e,r){const s=w(e);let o;try{o=w(f(s,r))}catch{throw new Error("yolo-spec-envelope-required")}if(!o.startsWith(`${s}${b}`))throw new Error("yolo-spec-envelope-path-out-of-scope");if(!R(o).isFile())throw new Error("yolo-spec-envelope-unsafe");if(R(o).size>O)throw new Error("yolo-spec-envelope-too-large");return $(o,"utf8")}function m(e,r){const o=v(e,{context:r==="story"?"story-target":"epic-target",explicitlySelected:!0});if(o.status==="malformed"||o.status==="oversized")throw new Error("yolo-spec-envelope-unsafe");if(o.status==="absent")return e;const c=e.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);if(!c)throw new Error("yolo-spec-envelope-unsafe");return`---
2
+ ${Object.entries(o.metadata).map(([n,l])=>`${n}: ${JSON.stringify(l)}`).join(`
3
+ `)}
4
+ ---
5
+ ${e.slice(c[0].length)}`}function _(e){try{return L("git",["-C",e,"rev-parse","HEAD"],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{throw new Error("yolo-spec-envelope-stale")}}function H(e){const r=`docs/stories/${e.storyId}.story.md`,s=`docs/epics/epic-${e.epicId}.md`,o=e.headSha??_(e.projectRoot),c=m(a(e.projectRoot,r),"story"),u=m(a(e.projectRoot,s),"epic"),n=[{kind:"story",path:r,content:c},{kind:"epic",path:s,content:u}],l=new Set([`docs/stories/${e.storyId}.spec.md`,`docs/stories/${e.storyId}.tasks.md`,`docs/stories/${e.storyId}.rtm.md`]),h=[...new Set([...z(c),...[...l].filter(t=>S(f(e.projectRoot,t)))])].sort(),T=[...h.filter(t=>l.has(t)),...h.filter(t=>!l.has(t))];let p=n.reduce((t,i)=>t+Buffer.byteLength(i.content,"utf8"),0),y=0;for(const t of T){if(t===r||t===s||!S(f(e.projectRoot,t)))continue;if(t.startsWith("docs/stories/")&&t.endsWith(".story.md")||t.startsWith("docs/epics/"))throw new Error("yolo-spec-envelope-target-mismatch");const i=a(e.projectRoot,t),d=Buffer.byteLength(i,"utf8");if(n.length>=g||p+d>I){if(l.has(t))throw new Error("yolo-spec-envelope-too-large");y+=1;continue}n.push({kind:A(t),path:t,content:i}),p+=d}y&&e.onOmittedReferences?.(y);for(const t of C){if(t===r||t===s||h.includes(t)||!S(f(e.projectRoot,t)))continue;const i=a(e.projectRoot,t),d=Buffer.byteLength(i,"utf8");n.length>=g||p+d>I||(n.push({kind:A(t),path:t,content:i}),p+=d)}const j=E({storyId:e.storyId,epicId:e.epicId,targetPath:r,inputSha256:e.inputSha256,headSha:o,collectedAt:e.collectedAt??new Date().toISOString(),artifacts:n}),x=e.headSha??_(e.projectRoot);if(o!==x||m(a(e.projectRoot,r),"story")!==c||m(a(e.projectRoot,s),"epic")!==u||n.some(t=>t.kind!=="story"&&t.kind!=="epic"&&a(e.projectRoot,t.path)!==t.content))throw new Error("yolo-spec-envelope-stale");return j}function D(e,r=new Date().toISOString()){return E({storyId:e.storyId,epicId:e.epicId,targetPath:e.targetPath,inputSha256:e.inputSha256,headSha:e.sourceRevision.headSha,collectedAt:r,artifacts:e.artifacts.map(({kind:s,path:o,content:c})=>({kind:s,path:o,content:c}))})}export{H as collectYoloSpecEnvelope,Z as extractTargetedYoloStoryId,D as refreshPrecollectedYoloSpecEnvelope};
@@ -1,300 +1,2 @@
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
- * Epic P120.04 -- Client-side discovery hook (extends P101.07 pattern).
15
- *
16
- * Pre-request hook that, when `YOLOOP_AUTO_DISCOVERY=on` and trigger is
17
- * `*yoloop` resolving to a directory, calls `discoverEpicsFromDir()` (P120.01)
18
- * + `mergeDiscoveredIntoState()` (P120.02) on the client BEFORE sending the
19
- * invoke request. Server (P120.05) sees the enriched state in
20
- * `body.stateSnapshot.epics`, relaxing `EPIC_NOT_FOUND` for filesystem epics.
21
- *
22
- * Hook follows P101.07 pattern (lazy dynamic import for tree-shaking via
23
- * the wrapper in `invoke-hooks.ts`, fail-soft try/catch, mutate stateSnapshot
24
- * in place).
25
- *
26
- * Activation conditions (ALL must hold):
27
- * 1. `featureFlag === true` -- operator opt-in via `YOLOOP_AUTO_DISCOVERY=on`
28
- * 2. Args resolve to a yoloop trigger (regex per P101.07)
29
- * 3. Args target via `@<path>` resolves to a directory
30
- *
31
- * Fail-open contract: ANY error in discovery / merge / fs-resolution is
32
- * caught + logged (warn) + returns `{ applied: false, reason }`. The
33
- * caller (invoke.ts wrapper) ALSO catches outermost. Hook NEVER blocks
34
- * the invoke request. Worst case: state is unenriched + server falls back
35
- * to existing canonical resolver (pre-P120 behavior).
36
- *
37
- * Pattern references:
38
- * - P101.07 (yoloop pre-request hook) -- exact pattern reused
39
- * - P120.01 (discoverEpicsFromDir) -- consumed via local SSoT-Shadow inline
40
- * - P120.02 (mergeDiscoveredIntoState) -- consumed via local SSoT-Shadow inline
41
- * - P120.03 (feature flag) -- gating variable
42
- * - P107.02 (architecture-policy hook) -- secondary reference for
43
- * fail-soft + dynamic import + state mutation
44
- *
45
- * Pre-request hook order (client-side, before invoke POST):
46
- * 1. P121.05: maybeAutoResolveYoloopArgs (invoke-hooks.ts) -- rewrite empty args
47
- * 2. P101.07: yoloop client state collection -- read ~/.neocortex/yoloop-state.json
48
- * 3. P120.04: maybeRunDiscoveryHook (this hook) -- scan args dir for epics
49
- * 4. P107.02: architecture-policy reader -- attach state.architecture_policy
50
- * 5. POST /invoke
51
- *
52
- * P121.05 runs FIRST so that subsequent hooks (P101.07, this hook) see the
53
- * rewritten args. The discovery hook itself sees `*yoloop @docs/epics/`
54
- * after P121.05 fires and proceeds normally.
55
- */
56
- import { existsSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
57
- import { basename, dirname, resolve, isAbsolute } from 'node:path';
58
- // P120.04: import from local SSoT-Shadow inline mirror (P117.01 pattern).
59
- // See `./shared-discover-epics.ts` for sync rules and drift test enforcement
60
- // in `client/__tests__/discover-shared-sync.test.ts`.
61
- import { discoverEpicsFromDir, mergeDiscoveredIntoState, buildDefaultDiscoveryFs, } from './shared-discover-epics.js';
62
- import { extractYoloopTargetPath, isYoloopInvocation, } from './target-normalization.js';
63
- // =====================================================================
64
- // Internal helpers
65
- // =====================================================================
66
- /**
67
- * Resolve a `@<path>` token to an absolute path, given a project root.
68
- * Pure function: zero I/O.
69
- */
70
- function resolveAtPath(token, projectRoot) {
71
- if (isAbsolute(token))
72
- return token;
73
- return resolve(projectRoot, token);
74
- }
75
- /**
76
- * Keep on-disk `.neocortex/state.json` aligned with the enriched snapshot.
77
- *
78
- * The server receives `stateSnapshot`, so snapshot mutation is sufficient for
79
- * the current invoke. Persisting discovered epics removes the next-iteration
80
- * EPIC_NOT_FOUND failure and matches the operator expectation that filesystem
81
- * epics are registered once discovered. This writer reads state.json fresh so
82
- * transient request-only fields (for example `stateSnapshot.yoloop`) are not
83
- * accidentally stored in the project state.
84
- */
85
- function persistDiscoveredStateJson(projectRoot, discovered, logger) {
86
- if (discovered.epics.length === 0)
87
- return;
88
- const statePath = resolve(projectRoot, '.neocortex', 'state.json');
89
- if (!existsSync(statePath))
90
- return;
91
- try {
92
- const raw = readFileSync(statePath, 'utf8');
93
- const diskState = JSON.parse(raw);
94
- const merge = mergeDiscoveredIntoState(diskState, discovered);
95
- if (merge.added.length === 0 && merge.storiesAdded === 0)
96
- return;
97
- diskState.last_updated = new Date().toISOString();
98
- const tmpPath = `${statePath}.tmp.${process.pid}.${Date.now()}`;
99
- writeFileSync(tmpPath, `${JSON.stringify(diskState, null, 2)}\n`, 'utf8');
100
- renameSync(tmpPath, statePath);
101
- }
102
- catch (err) {
103
- const msg = err instanceof Error ? err.message : String(err);
104
- logger?.warn?.(`[neocortex] discovery hook: failed to persist .neocortex/state.json: ${msg}. Proceeding with request snapshot only.`);
105
- }
106
- }
107
- function filterDiscoveryToFile(result, absPath) {
108
- const targetBase = basename(absPath);
109
- const epics = result.epics.filter((epic) => {
110
- return epic.path === absPath || epic.filename === targetBase;
111
- });
112
- const epicIds = new Set(epics.map((epic) => epic.id));
113
- return {
114
- epics: Object.freeze(epics),
115
- warnings: Object.freeze([
116
- ...result.warnings,
117
- ...(epics.length === 0
118
- ? [`single-file discovery found no matching epic descriptor for ${targetBase}`]
119
- : []),
120
- ]),
121
- skipped: Object.freeze([
122
- ...result.skipped,
123
- ...result.epics
124
- .filter((epic) => !epicIds.has(epic.id))
125
- .map((epic) => `${epic.filename}: skipped (not requested file)`),
126
- ]),
127
- };
128
- }
129
- // =====================================================================
130
- // Public API
131
- // =====================================================================
132
- /**
133
- * Run discovery + merge if hook conditions are met.
134
- *
135
- * Async because `buildDefaultDiscoveryFs()` lazy-imports `node:fs` /
136
- * `node:path` (tree-shake friendly per P120.01 design). Returns
137
- * synchronously-resolvable promise in the no-op cases (flag off, non-yoloop
138
- * trigger, file not dir, missing dir) -- discovery work only happens on
139
- * the happy path.
140
- *
141
- * **Never throws**. ANY error is caught, logged via `logger.warn`, and
142
- * returned as `{ applied: false, reason }`. Caller continues normal
143
- * invoke flow without enrichment.
144
- *
145
- * @param ctx hook context (args + projectRoot + stateSnapshot + flag + logger)
146
- * @returns DiscoveryHookResult with telemetry counters
147
- */
148
- export async function runDiscoveryHook(ctx) {
149
- // -------------------------------------------------------------------
150
- // Activation conditions (ordered cheap-to-expensive)
151
- // -------------------------------------------------------------------
152
- // 1. Feature flag check (zero I/O, fastest)
153
- if (!ctx.featureFlag) {
154
- return {
155
- applied: false,
156
- added: Object.freeze([]),
157
- skipped: Object.freeze([]),
158
- storiesAdded: 0,
159
- reason: 'flag off',
160
- };
161
- }
162
- // 2. Yoloop trigger detection (regex over args)
163
- if (!isYoloopInvocation(ctx.args)) {
164
- return {
165
- applied: false,
166
- added: Object.freeze([]),
167
- skipped: Object.freeze([]),
168
- storiesAdded: 0,
169
- reason: 'not a yoloop trigger',
170
- };
171
- }
172
- // 3. Extract target path arg
173
- const pathToken = extractYoloopTargetPath(ctx.args, ctx.projectRoot);
174
- if (!pathToken) {
175
- return {
176
- applied: false,
177
- added: Object.freeze([]),
178
- skipped: Object.freeze([]),
179
- storiesAdded: 0,
180
- reason: 'no path arg',
181
- };
182
- }
183
- // 4. Resolve to absolute path
184
- let absPath;
185
- try {
186
- absPath = resolveAtPath(pathToken, ctx.projectRoot);
187
- }
188
- catch (err) {
189
- const msg = err instanceof Error ? err.message : String(err);
190
- ctx.logger?.warn?.(`[neocortex] discovery hook: path resolution failed: ${msg}. Proceeding without enrichment.`);
191
- return {
192
- applied: false,
193
- added: Object.freeze([]),
194
- skipped: Object.freeze([]),
195
- storiesAdded: 0,
196
- reason: 'path resolution failed',
197
- };
198
- }
199
- // 5. Verify path exists and determine scan mode.
200
- let isDir = false;
201
- let isFile = false;
202
- try {
203
- if (existsSync(absPath)) {
204
- const stat = statSync(absPath);
205
- isDir = stat.isDirectory();
206
- isFile = stat.isFile();
207
- }
208
- }
209
- catch {
210
- // statSync error -> treat as missing (fail-open)
211
- isDir = false;
212
- isFile = false;
213
- }
214
- if (!isDir && !isFile) {
215
- return {
216
- applied: false,
217
- added: Object.freeze([]),
218
- skipped: Object.freeze([]),
219
- storiesAdded: 0,
220
- reason: 'path not found',
221
- };
222
- }
223
- // -------------------------------------------------------------------
224
- // Activation: run discovery + merge
225
- // -------------------------------------------------------------------
226
- try {
227
- const fs = await buildDefaultDiscoveryFs();
228
- const scanDir = isDir ? absPath : dirname(absPath);
229
- const rawResult = discoverEpicsFromDir(scanDir, fs);
230
- const result = isFile ? filterDiscoveryToFile(rawResult, absPath) : rawResult;
231
- // Even if zero epics discovered, we still count this as "applied" for
232
- // observability (caller knows hook ran). State stays untouched.
233
- const merge = mergeDiscoveredIntoState(ctx.stateSnapshot, result);
234
- persistDiscoveredStateJson(ctx.projectRoot, result, ctx.logger);
235
- // Telemetry: emit info if anything was added
236
- if (merge.added.length > 0 || merge.storiesAdded > 0) {
237
- ctx.logger?.info?.(`[neocortex] discovered ${merge.added.length} epic(s), ${merge.storiesAdded} story-ies from ${isDir ? absPath : basename(absPath)}`);
238
- }
239
- // Surface non-fatal warnings (e.g., orphan stories, ID collisions)
240
- for (const w of result.warnings) {
241
- ctx.logger?.warn?.(`[neocortex] discovery: ${w}`);
242
- }
243
- return {
244
- applied: true,
245
- added: merge.added,
246
- skipped: merge.skipped,
247
- storiesAdded: merge.storiesAdded,
248
- };
249
- }
250
- catch (err) {
251
- const msg = err instanceof Error ? err.message : String(err);
252
- ctx.logger?.warn?.(`[neocortex] discovery hook failed: ${msg}. Proceeding without enrichment.`);
253
- return {
254
- applied: false,
255
- added: Object.freeze([]),
256
- skipped: Object.freeze([]),
257
- storiesAdded: 0,
258
- reason: `error: ${msg}`,
259
- };
260
- }
261
- }
262
- /**
263
- * Async wrapper with lazy-load semantics + outermost defensive try/catch.
264
- *
265
- * Designed to be invoked from `invoke-hooks.ts`. Performs the same
266
- * `featureFlag` short-circuit as `runDiscoveryHook` to keep the async
267
- * import truly tree-shakeable when the flag is off. Wraps the inner call
268
- * in an additional try/catch as defense-in-depth (the hook itself is
269
- * already fail-open, but module-load errors are caught here).
270
- */
271
- export async function maybeRunDiscoveryHook(ctx) {
272
- // Defense-in-depth: short-circuit BEFORE doing any work if flag is off.
273
- // Note: in practice, callers (invoke-hooks.ts) also check the flag
274
- // before importing this module to maximize tree-shaking. This is a
275
- // belt-and-suspenders guard.
276
- if (!ctx.featureFlag) {
277
- return {
278
- applied: false,
279
- added: Object.freeze([]),
280
- skipped: Object.freeze([]),
281
- storiesAdded: 0,
282
- reason: 'flag off',
283
- };
284
- }
285
- try {
286
- return await runDiscoveryHook(ctx);
287
- }
288
- catch (err) {
289
- // runDiscoveryHook is itself fail-open; this is defense-in-depth.
290
- const msg = err instanceof Error ? err.message : String(err);
291
- ctx.logger?.warn?.(`[neocortex] discovery hook (outer) failed: ${msg}. Proceeding without enrichment.`);
292
- return {
293
- applied: false,
294
- added: Object.freeze([]),
295
- skipped: Object.freeze([]),
296
- storiesAdded: 0,
297
- reason: `outer error: ${msg}`,
298
- };
299
- }
300
- }
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,8 +1 @@
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, } from './yoloop-client-state-store.js';
8
- export { isYoloopTrigger, readYoloopForRequest, persistYoloopFromResponse, maybeRunDiscoveryHook, } from './invoke-hooks.js';
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};