@ornexus/neocortex 4.60.73 → 4.60.74

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 (241) hide show
  1. package/dist/release-source.json +1 -1
  2. package/dist/sbom.cdx.json +1216 -1210
  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 +5 -358
  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.js +143 -7200
  38. package/packages/client/dist/commands/legacy-epic-terminal.js +1 -71
  39. package/packages/client/dist/commands/legacy-story-branch.js +1 -96
  40. package/packages/client/dist/commands/legacy-story-pr.js +1 -276
  41. package/packages/client/dist/commands/mutation-qa-checkpoint.js +5 -285
  42. package/packages/client/dist/commands/mutation-qa-gate.js +1 -54
  43. package/packages/client/dist/commands/mutation-qa-git.js +1 -41
  44. package/packages/client/dist/commands/mutation-qa-policy.js +1 -102
  45. package/packages/client/dist/commands/mutation-qa-run.js +3 -219
  46. package/packages/client/dist/commands/mutation-qa-verdict.js +1 -149
  47. package/packages/client/dist/commands/mutation-qa.js +1 -220
  48. package/packages/client/dist/commands/native-agent.js +2 -136
  49. package/packages/client/dist/commands/planning-diagnosis.js +1 -43
  50. package/packages/client/dist/commands/planning-generation.js +1 -53
  51. package/packages/client/dist/commands/planning-memory.js +1 -40
  52. package/packages/client/dist/commands/planning-research.js +1 -42
  53. package/packages/client/dist/commands/private-session.js +1 -75
  54. package/packages/client/dist/commands/property-qa.js +1 -139
  55. package/packages/client/dist/commands/qa-control.js +1 -16
  56. package/packages/client/dist/commands/qa-scan-browser-launch.js +1 -55
  57. package/packages/client/dist/commands/qa-scan-browser-route.js +1 -28
  58. package/packages/client/dist/commands/qa-scan-capture.js +5 -663
  59. package/packages/client/dist/commands/qa-scan-discovery.js +3 -376
  60. package/packages/client/dist/commands/qa-scan-review.js +5 -826
  61. package/packages/client/dist/commands/qa-synthetic-backend-capture.js +1 -266
  62. package/packages/client/dist/commands/qa-synthetic-backend-review.js +1 -297
  63. package/packages/client/dist/commands/qa-synthetic-backend.js +2 -340
  64. package/packages/client/dist/commands/qa-synthetic-broker-port.js +1 -85
  65. package/packages/client/dist/commands/qa-synthetic-browser.js +1 -39
  66. package/packages/client/dist/commands/refactor-characterization.js +21 -671
  67. package/packages/client/dist/commands/refactor-commit.js +13 -399
  68. package/packages/client/dist/commands/refactor-control.js +3 -142
  69. package/packages/client/dist/commands/refactor-cpp-assertions.js +3 -93
  70. package/packages/client/dist/commands/refactor-review.js +5 -283
  71. package/packages/client/dist/commands/refactor-root.js +7 -588
  72. package/packages/client/dist/commands/refresh-memory.js +3 -156
  73. package/packages/client/dist/commands/shared-architecture-non-ui.js +1 -24
  74. package/packages/client/dist/commands/story-checkpoint.js +1 -171
  75. package/packages/client/dist/config/profile-directory.js +1 -13
  76. package/packages/client/dist/config/resolver-selection.js +1 -278
  77. package/packages/client/dist/config/secure-config.js +12 -281
  78. package/packages/client/dist/config.js +2 -223
  79. package/packages/client/dist/constants.js +1 -25
  80. package/packages/client/dist/context/context-collector.js +2 -222
  81. package/packages/client/dist/context/context-sanitizer.js +1 -145
  82. package/packages/client/dist/continuity/autonomous-governance.js +1 -285
  83. package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1587
  84. package/packages/client/dist/continuity/invoke-hooks.js +1 -129
  85. package/packages/client/dist/continuity/migrations/001-initial-schema.js +2 -12
  86. package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +2 -12
  87. package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +2 -12
  88. package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +2 -12
  89. package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +2 -12
  90. package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +2 -10
  91. package/packages/client/dist/continuity/native-agent-store.js +1 -69
  92. package/packages/client/dist/continuity/native-agent-types.js +1 -90
  93. package/packages/client/dist/continuity/outbox.js +1 -117
  94. package/packages/client/dist/continuity/runner-bootstrap-policy.js +1 -172
  95. package/packages/client/dist/continuity/sqlite-store.js +81 -2612
  96. package/packages/client/dist/effects/invoke-effect-gateways.js +1 -62
  97. package/packages/client/dist/errors/error-messages.js +2 -228
  98. package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -125
  99. package/packages/client/dist/evidence/collector.js +1 -347
  100. package/packages/client/dist/evidence/index.js +1 -1
  101. package/packages/client/dist/evidence/types.js +0 -2
  102. package/packages/client/dist/fs/atomic-state-replace.js +1 -152
  103. package/packages/client/dist/generated/command-registry.generated.js +1 -4076
  104. package/packages/client/dist/generated/public-projections.generated.js +1 -149
  105. package/packages/client/dist/git/direct-yolo-c01-retry.js +4 -138
  106. package/packages/client/dist/git/direct-yolo-checkpoint.js +4 -1234
  107. package/packages/client/dist/git/epic-operational-state.js +2 -672
  108. package/packages/client/dist/git/git-lifecycle-step-ids.js +1 -14
  109. package/packages/client/dist/git/git-lifecycle-store.d.ts +23 -0
  110. package/packages/client/dist/git/git-lifecycle-store.js +10 -1981
  111. package/packages/client/dist/git/legacy-story-branch-checkpoint.js +9 -259
  112. package/packages/client/dist/git/legacy-story-branch-setup.js +4 -348
  113. package/packages/client/dist/git/legacy-story-pr.js +70 -3538
  114. package/packages/client/dist/git/secure-filesystem-boundary.js +5 -182
  115. package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -284
  116. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -237
  117. package/packages/client/dist/i18n/first-run.js +2 -60
  118. package/packages/client/dist/index.js +1 -47
  119. package/packages/client/dist/invocation/invoke-input.js +1 -24
  120. package/packages/client/dist/license/index.js +1 -5
  121. package/packages/client/dist/license/license-client.js +1 -304
  122. package/packages/client/dist/machine/fingerprint.js +2 -163
  123. package/packages/client/dist/machine/index.js +1 -5
  124. package/packages/client/dist/memory/project-memory-path-scope.js +1 -62
  125. package/packages/client/dist/memory/project-memory-writer.js +40 -1007
  126. package/packages/client/dist/memory/shared-project-memory-types.js +2 -253
  127. package/packages/client/dist/migrations/epic-lifecycle-backfill.js +3 -1060
  128. package/packages/client/dist/migrations/project-identity-capability.js +1 -50
  129. package/packages/client/dist/migrations/project-identity.js +7 -493
  130. package/packages/client/dist/migrations/state-json.js +4 -393
  131. package/packages/client/dist/policy/architecture-policy.js +2 -184
  132. package/packages/client/dist/policy/index.js +1 -7
  133. package/packages/client/dist/policy/shared-policy-types.js +0 -38
  134. package/packages/client/dist/policy/subagent-approval-policy.js +1 -55
  135. package/packages/client/dist/private-execution/clean-baseline.js +2 -279
  136. package/packages/client/dist/private-execution/codex-executor.js +4 -574
  137. package/packages/client/dist/private-execution/codex-host-runtime.js +1 -137
  138. package/packages/client/dist/private-execution/codex-lifecycle-guard.js +1 -97
  139. package/packages/client/dist/private-execution/codex-scoped-permissions.js +1 -128
  140. package/packages/client/dist/private-execution/delivery.js +13 -410
  141. package/packages/client/dist/private-execution/diagnosis-root-review.js +2 -294
  142. package/packages/client/dist/private-execution/frozen-handoff.js +6 -19
  143. package/packages/client/dist/private-execution/implementation-validation.js +2 -106
  144. package/packages/client/dist/private-execution/isolated-admission.js +23 -1432
  145. package/packages/client/dist/private-execution/isolated-session.js +5 -725
  146. package/packages/client/dist/private-execution/isolated-workspace.js +1 -122
  147. package/packages/client/dist/private-execution/pact-consumer-runtime.js +3 -409
  148. package/packages/client/dist/private-execution/pact-test-source.js +1 -258
  149. package/packages/client/dist/private-execution/pact-toolchain.js +3 -290
  150. package/packages/client/dist/private-execution/pact-write-set.js +1 -172
  151. package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +5 -406
  152. package/packages/client/dist/private-execution/planning-generation-checkpoint.js +5 -462
  153. package/packages/client/dist/private-execution/planning-memory-checkpoint.js +3 -139
  154. package/packages/client/dist/private-execution/planning-research-checkpoint.js +4 -325
  155. package/packages/client/dist/private-execution/private-report-store.js +1 -42
  156. package/packages/client/dist/private-execution/property-test-candidate.js +1 -103
  157. package/packages/client/dist/private-execution/property-test-choices.js +3 -147
  158. package/packages/client/dist/private-execution/property-test-plan.js +7 -271
  159. package/packages/client/dist/private-execution/property-test-review.js +4 -258
  160. package/packages/client/dist/private-execution/property-test-runtime.js +9 -372
  161. package/packages/client/dist/private-execution/property-test-source.js +1 -139
  162. package/packages/client/dist/private-execution/root-checkpoint.js +1 -80
  163. package/packages/client/dist/private-execution/root-memory-git-evidence.js +3 -135
  164. package/packages/client/dist/private-execution/root-memory-recovery.js +1 -93
  165. package/packages/client/dist/private-execution/root-memory-request.js +1 -29
  166. package/packages/client/dist/private-execution/root-memory-scope.js +1 -178
  167. package/packages/client/dist/private-execution/root-memory-writer.js +1 -102
  168. package/packages/client/dist/private-execution/session-lease.js +1 -63
  169. package/packages/client/dist/private-execution/shared-private-admission-types.js +1 -50
  170. package/packages/client/dist/private-execution/shared-project-validation.js +1 -66
  171. package/packages/client/dist/private-execution/shared-root-memory-types.js +1 -61
  172. package/packages/client/dist/private-execution/ux-source-validation.js +1 -372
  173. package/packages/client/dist/private-execution/ux-write-set.js +2 -191
  174. package/packages/client/dist/registry/registry-reconciliation.js +1 -234
  175. package/packages/client/dist/resilience/circuit-breaker.js +1 -170
  176. package/packages/client/dist/resilience/degradation-manager.js +1 -164
  177. package/packages/client/dist/resilience/freshness-indicator.js +1 -100
  178. package/packages/client/dist/resilience/index.js +1 -8
  179. package/packages/client/dist/resilience/recovery-detector.js +1 -74
  180. package/packages/client/dist/resolvers/asset-resolver.js +0 -13
  181. package/packages/client/dist/resolvers/local-resolver.js +8 -255
  182. package/packages/client/dist/resolvers/remote-resolver.js +1 -233
  183. package/packages/client/dist/runner/binary.js +1 -404
  184. package/packages/client/dist/runner/cli.js +17 -1571
  185. package/packages/client/dist/runner/headless/adapters.js +1 -49
  186. package/packages/client/dist/runner/headless/claude-code-adapter.js +1 -79
  187. package/packages/client/dist/runner/headless/continuous-story-workspace.js +11 -1628
  188. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -69
  189. package/packages/client/dist/runner/headless/epic-merge-candidate-qa.d.ts +1 -1
  190. package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +2 -110
  191. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.d.ts +2 -0
  192. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +10 -1236
  193. package/packages/client/dist/runner/headless/epic-terminal-git-runner.d.ts +18 -2
  194. package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +2 -318
  195. package/packages/client/dist/runner/headless/fake-adapter.js +1 -62
  196. package/packages/client/dist/runner/headless/managed-profile.js +1 -132
  197. package/packages/client/dist/runner/headless/opencode-adapter.js +2 -145
  198. package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -43
  199. package/packages/client/dist/runner/headless/resolver.js +1 -231
  200. package/packages/client/dist/runner/headless/run-yolo.js +1 -885
  201. package/packages/client/dist/runner/headless/run-yoloop.d.ts +1 -0
  202. package/packages/client/dist/runner/headless/run-yoloop.js +2 -2574
  203. package/packages/client/dist/runner/headless/types.js +2 -574
  204. package/packages/client/dist/runner/local-console.js +13 -396
  205. package/packages/client/dist/runner/portal-sync-worker.js +1 -1487
  206. package/packages/client/dist/runner/project-identity.js +1 -106
  207. package/packages/client/dist/runner/scheduler.js +7 -995
  208. package/packages/client/dist/runner-cli.js +2 -18
  209. package/packages/client/dist/state/invocation-shape.js +1 -13
  210. package/packages/client/dist/state/project-state-snapshot.js +1 -1731
  211. package/packages/client/dist/state/state-json-repair.js +3 -785
  212. package/packages/client/dist/state/structural-dag-projection.js +1 -118
  213. package/packages/client/dist/telemetry/index.js +1 -5
  214. package/packages/client/dist/telemetry/offline-queue.js +1 -131
  215. package/packages/client/dist/tier/index.js +1 -5
  216. package/packages/client/dist/tier/tier-aware-client.js +1 -405
  217. package/packages/client/dist/types/index.js +1 -38
  218. package/packages/client/dist/version.js +1 -27
  219. package/packages/client/dist/yolo/change-evidence-collector.js +1 -126
  220. package/packages/client/dist/yolo/invoke-envelope-capability.js +1 -62
  221. package/packages/client/dist/yolo/spec-envelope-collector.js +5 -184
  222. package/packages/client/dist/yoloop/discovery-hook.js +2 -300
  223. package/packages/client/dist/yoloop/index.js +1 -8
  224. package/packages/client/dist/yoloop/invoke-hooks.js +12 -669
  225. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1007
  226. package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -33
  227. package/packages/client/dist/yoloop/target-normalization.js +1 -180
  228. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1920
  229. package/packages/shared/dist/yolo/change-evidence.js +1 -1
  230. package/packages/shared/dist/yoloop/epic-terminal-git.js +144 -2
  231. package/targets-stubs/antigravity/gemini.md +1 -1
  232. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  233. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  234. package/targets-stubs/claude-code/neocortex-root.md +2 -2
  235. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  236. package/targets-stubs/claude-code/neocortex.md +2 -2
  237. package/targets-stubs/codex/AGENTS.md +1 -1
  238. package/targets-stubs/cursor/agent.md +2 -2
  239. package/targets-stubs/gemini-cli/agent.md +2 -2
  240. package/targets-stubs/opencode/neocortex-root.md +1 -1
  241. 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,184 +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_MAX_ARTIFACTS, } 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
- // Resource-safety stop only. Ordinary envelope sizing belongs to the existing
16
- // invoke request guardrail, not to a smaller 64/128 KiB document policy.
17
- const YOLO_SPEC_LOCAL_SOURCE_SAFETY_LIMIT_BYTES = 16 * 1024 * 1024;
18
- /** Resolve only an explicit story target. Inline YOLO text intentionally has no match. */
19
- export function extractTargetedYoloStoryId(args) {
20
- const pathMatches = [...args.matchAll(/(?:^|\s)@?docs\/stories\/([A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+)\.story\.md(?=\s|$)/g)]
21
- .map((match) => match[1]);
22
- const tokenMatches = args.trim().split(/\s+/).slice(1)
23
- .map((token) => token.replace(/^@/, ''))
24
- .filter((token) => STORY_ID.test(token));
25
- const matches = [...new Set([...pathMatches, ...tokenMatches])];
26
- return matches.length === 1 ? matches[0] : null;
27
- }
28
- function artifactKind(path) {
29
- if (path.startsWith('core/templates/'))
30
- return 'template';
31
- if (path.startsWith('docs/epics/'))
32
- return 'epic';
33
- if (path.startsWith('docs/stories/') && path.endsWith('.story.md'))
34
- return 'story';
35
- if (path.endsWith('.rtm.md'))
36
- return 'traceability';
37
- if (path.toLowerCase().includes('adr'))
38
- return 'adr';
39
- if (path.toLowerCase().includes('contract'))
40
- return 'contract';
41
- if (path.toLowerCase().includes('nfr'))
42
- return 'nfr';
43
- if (path.endsWith('.feature'))
44
- return 'bdd';
45
- if (path.toLowerCase().includes('traceability'))
46
- return 'traceability';
47
- if (path.startsWith('docs/examples/'))
48
- return 'example';
49
- return 'spec';
50
- }
51
- function selectedReferencePaths(story) {
52
- return [...new Set([...story.matchAll(REFERENCE_PATH)].map((match) => match[1]).filter((path) => Boolean(path)))].sort();
53
- }
54
- function safeSource(root, path) {
55
- const rootReal = realpathSync(root);
56
- let fileReal;
57
- try {
58
- fileReal = realpathSync(join(rootReal, path));
59
- }
60
- catch {
61
- throw new Error('yolo-spec-envelope-required');
62
- }
63
- if (!fileReal.startsWith(`${rootReal}${sep}`))
64
- throw new Error('yolo-spec-envelope-path-out-of-scope');
65
- const sourceStat = statSync(fileReal);
66
- if (!sourceStat.isFile())
67
- throw new Error('yolo-spec-envelope-unsafe');
68
- if (sourceStat.size > YOLO_SPEC_LOCAL_SOURCE_SAFETY_LIMIT_BYTES)
69
- throw new Error('yolo-spec-envelope-too-large');
70
- return readFileSync(fileReal, 'utf8');
71
- }
72
- function publicMarkdown(source, kind) {
73
- const context = kind === 'story' ? 'story-target' : 'epic-target';
74
- const inspected = inspectFrontmatterContext(source, { context, explicitlySelected: true });
75
- if (inspected.status === 'malformed' || inspected.status === 'oversized')
76
- throw new Error('yolo-spec-envelope-unsafe');
77
- if (inspected.status === 'absent')
78
- return source;
79
- const match = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
80
- if (!match)
81
- throw new Error('yolo-spec-envelope-unsafe');
82
- const fields = Object.entries(inspected.metadata).map(([key, value]) => `${key}: ${JSON.stringify(value)}`);
83
- return `---\n${fields.join('\n')}\n---\n${source.slice(match[0].length)}`;
84
- }
85
- function readHeadSha(root) {
86
- try {
87
- return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], {
88
- encoding: 'utf8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'],
89
- }).trim();
90
- }
91
- catch {
92
- throw new Error('yolo-spec-envelope-stale');
93
- }
94
- }
95
- export function collectYoloSpecEnvelope(options) {
96
- const targetPath = `docs/stories/${options.storyId}.story.md`;
97
- const epicPath = `docs/epics/epic-${options.epicId}.md`;
98
- const initialHead = options.headSha ?? readHeadSha(options.projectRoot);
99
- const story = publicMarkdown(safeSource(options.projectRoot, targetPath), 'story');
100
- const epic = publicMarkdown(safeSource(options.projectRoot, epicPath), 'epic');
101
- const artifacts = [
102
- { kind: 'story', path: targetPath, content: story },
103
- { kind: 'epic', path: epicPath, content: epic },
104
- ];
105
- const coreStoryReferences = new Set([
106
- `docs/stories/${options.storyId}.spec.md`,
107
- `docs/stories/${options.storyId}.tasks.md`,
108
- `docs/stories/${options.storyId}.rtm.md`,
109
- ]);
110
- // Older stories may not link to their own SDD files. Bind physically present
111
- // siblings to the same HEAD/content checks as explicitly referenced files.
112
- const selectedReferences = [...new Set([
113
- ...selectedReferencePaths(story),
114
- ...[...coreStoryReferences].filter((path) => existsSync(join(options.projectRoot, path))),
115
- ])].sort();
116
- const prioritizedReferences = [
117
- ...selectedReferences.filter((path) => coreStoryReferences.has(path)),
118
- ...selectedReferences.filter((path) => !coreStoryReferences.has(path)),
119
- ];
120
- let omittedReferences = 0;
121
- for (const path of prioritizedReferences) {
122
- if (path === targetPath || path === epicPath || !existsSync(join(options.projectRoot, path)))
123
- continue;
124
- // A foreign story/epic is a target conflict, never optional context. The
125
- // byte budget must not turn a prior target-mismatch denial into admission.
126
- if ((path.startsWith('docs/stories/') && path.endsWith('.story.md'))
127
- || path.startsWith('docs/epics/'))
128
- throw new Error('yolo-spec-envelope-target-mismatch');
129
- const content = safeSource(options.projectRoot, path);
130
- if (artifacts.length >= YOLO_SPEC_MAX_ARTIFACTS) {
131
- if (coreStoryReferences.has(path))
132
- throw new Error('yolo-spec-envelope-too-large');
133
- omittedReferences += 1;
134
- continue;
135
- }
136
- artifacts.push({ kind: artifactKind(path), path, content });
137
- }
138
- // Templates and non-core references are supporting context. Preserve the
139
- // story's own SDD artifacts and signal omissions without exposing paths.
140
- if (omittedReferences)
141
- options.onOmittedReferences?.(omittedReferences);
142
- for (const path of TEMPLATE_PATHS) {
143
- if (path === targetPath || path === epicPath || selectedReferences.includes(path)
144
- || !existsSync(join(options.projectRoot, path)))
145
- continue;
146
- const content = safeSource(options.projectRoot, path);
147
- if (artifacts.length >= YOLO_SPEC_MAX_ARTIFACTS)
148
- continue;
149
- artifacts.push({ kind: artifactKind(path), path, content });
150
- }
151
- const envelope = createYoloSpecEnvelopeV1({
152
- storyId: options.storyId,
153
- epicId: options.epicId,
154
- targetPath,
155
- inputSha256: options.inputSha256,
156
- headSha: initialHead,
157
- collectedAt: options.collectedAt ?? new Date().toISOString(),
158
- artifacts,
159
- });
160
- const finalHead = options.headSha ?? readHeadSha(options.projectRoot);
161
- if (initialHead !== finalHead
162
- || publicMarkdown(safeSource(options.projectRoot, targetPath), 'story') !== story
163
- || publicMarkdown(safeSource(options.projectRoot, epicPath), 'epic') !== epic
164
- || artifacts.some((artifact) => artifact.kind !== 'story' && artifact.kind !== 'epic'
165
- && safeSource(options.projectRoot, artifact.path) !== artifact.content)) {
166
- throw new Error('yolo-spec-envelope-stale');
167
- }
168
- return envelope;
169
- }
170
- /**
171
- * Reissue a precollected physical snapshot with a fresh bounded transport time.
172
- * The caller still performs the final disk/HEAD collection and digest comparison.
173
- */
174
- export function refreshPrecollectedYoloSpecEnvelope(envelope, collectedAt = new Date().toISOString()) {
175
- return createYoloSpecEnvelopeV1({
176
- storyId: envelope.storyId,
177
- epicId: envelope.epicId,
178
- targetPath: envelope.targetPath,
179
- inputSha256: envelope.inputSha256,
180
- headSha: envelope.sourceRevision.headSha,
181
- collectedAt,
182
- artifacts: envelope.artifacts.map(({ kind, path, content }) => ({ kind, path, content })),
183
- });
184
- }
1
+ import{execFileSync as _}from"node:child_process";import{existsSync as u,readFileSync as j,realpathSync as y,statSync as x}from"node:fs";import{join as l,sep as $}from"node:path";import{createYoloSpecEnvelopeV1 as S,YOLO_SPEC_MAX_ARTIFACTS as w}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"],O=/(?:^|[\s([`'";])((?:docs\/(?:stories|epics|specs|architecture|examples)\/|core\/templates\/specs\/)[A-Za-z0-9_./-]+\.(?:md|mdx|feature))(?:$|[\s)\]`'",;])/gm,P=/^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+$/,T=16*1024*1024;function F(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=>P.test(c)),o=[...new Set([...r,...s])];return o.length===1?o[0]:null}function R(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 W(e){return[...new Set([...e.matchAll(O)].map(r=>r[1]).filter(r=>!!r))].sort()}function a(e,r){const s=y(e);let o;try{o=y(l(s,r))}catch{throw new Error("yolo-spec-envelope-required")}if(!o.startsWith(`${s}${$}`))throw new Error("yolo-spec-envelope-path-out-of-scope");const c=x(o);if(!c.isFile())throw new Error("yolo-spec-envelope-unsafe");if(c.size>T)throw new Error("yolo-spec-envelope-too-large");return j(o,"utf8")}function d(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,i])=>`${n}: ${JSON.stringify(i)}`).join(`
3
+ `)}
4
+ ---
5
+ ${e.slice(c[0].length)}`}function E(e){try{return _("git",["-C",e,"rev-parse","HEAD"],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{throw new Error("yolo-spec-envelope-stale")}}function Z(e){const r=`docs/stories/${e.storyId}.story.md`,s=`docs/epics/epic-${e.epicId}.md`,o=e.headSha??E(e.projectRoot),c=d(a(e.projectRoot,r),"story"),p=d(a(e.projectRoot,s),"epic"),n=[{kind:"story",path:r,content:c},{kind:"epic",path:s,content:p}],i=new Set([`docs/stories/${e.storyId}.spec.md`,`docs/stories/${e.storyId}.tasks.md`,`docs/stories/${e.storyId}.rtm.md`]),f=[...new Set([...W(c),...[...i].filter(t=>u(l(e.projectRoot,t)))])].sort(),g=[...f.filter(t=>i.has(t)),...f.filter(t=>!i.has(t))];let m=0;for(const t of g){if(t===r||t===s||!u(l(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 h=a(e.projectRoot,t);if(n.length>=w){if(i.has(t))throw new Error("yolo-spec-envelope-too-large");m+=1;continue}n.push({kind:R(t),path:t,content:h})}m&&e.onOmittedReferences?.(m);for(const t of C){if(t===r||t===s||f.includes(t)||!u(l(e.projectRoot,t)))continue;const h=a(e.projectRoot,t);n.length>=w||n.push({kind:R(t),path:t,content:h})}const A=S({storyId:e.storyId,epicId:e.epicId,targetPath:r,inputSha256:e.inputSha256,headSha:o,collectedAt:e.collectedAt??new Date().toISOString(),artifacts:n}),I=e.headSha??E(e.projectRoot);if(o!==I||d(a(e.projectRoot,r),"story")!==c||d(a(e.projectRoot,s),"epic")!==p||n.some(t=>t.kind!=="story"&&t.kind!=="epic"&&a(e.projectRoot,t.path)!==t.content))throw new Error("yolo-spec-envelope-stale");return A}function H(e,r=new Date().toISOString()){return S({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{Z as collectYoloSpecEnvelope,F as extractTargetedYoloStoryId,H 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};