@ornexus/neocortex 4.60.66 → 4.60.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/release-source.json +1 -1
- package/dist/sbom.cdx.json +1598 -1640
- package/install.ps1 +1 -1
- package/install.sh +2 -1
- package/package.json +10 -7
- package/packages/client/dist/adapters/adapter-registry.js +108 -1
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
- package/packages/client/dist/adapters/codex-adapter.js +80 -2
- package/packages/client/dist/adapters/cursor-adapter.js +115 -4
- package/packages/client/dist/adapters/gemini-adapter.js +71 -2
- package/packages/client/dist/adapters/grok-adapter.js +42 -2
- package/packages/client/dist/adapters/index.js +22 -1
- package/packages/client/dist/adapters/platform-detector.js +212 -1
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -2
- package/packages/client/dist/agent/refresh-stubs.js +417 -2
- package/packages/client/dist/agent/update-agent-yaml.js +102 -1
- package/packages/client/dist/agent/update-description.js +251 -1
- package/packages/client/dist/cache/crypto-utils.js +78 -1
- package/packages/client/dist/cache/encrypted-cache.js +108 -1
- package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
- package/packages/client/dist/cache/index.js +13 -1
- package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
- package/packages/client/dist/checkpoint/index.js +11 -1
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
- package/packages/client/dist/cli.js +303 -3
- package/packages/client/dist/commands/activate.js +394 -8
- package/packages/client/dist/commands/architecture-answers.js +274 -7
- package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
- package/packages/client/dist/commands/architecture-root-binding.js +51 -1
- package/packages/client/dist/commands/cache-status.js +112 -2
- package/packages/client/dist/commands/config.js +88 -1
- package/packages/client/dist/commands/doctor.d.ts +135 -0
- package/packages/client/dist/commands/doctor.js +635 -0
- package/packages/client/dist/commands/invoke.d.ts +18 -0
- package/packages/client/dist/commands/invoke.js +7140 -142
- package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
- package/packages/client/dist/commands/legacy-story-branch.js +96 -1
- package/packages/client/dist/commands/legacy-story-pr.js +276 -1
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
- package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
- package/packages/client/dist/commands/mutation-qa-git.js +41 -1
- package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
- package/packages/client/dist/commands/mutation-qa-run.js +219 -3
- package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
- package/packages/client/dist/commands/mutation-qa.js +220 -1
- package/packages/client/dist/commands/native-agent.js +136 -2
- package/packages/client/dist/commands/planning-diagnosis.js +43 -1
- package/packages/client/dist/commands/planning-generation.js +53 -1
- package/packages/client/dist/commands/planning-memory.js +40 -1
- package/packages/client/dist/commands/planning-research.js +42 -1
- package/packages/client/dist/commands/private-session.js +75 -1
- package/packages/client/dist/commands/property-qa.js +139 -1
- package/packages/client/dist/commands/qa-control.js +16 -1
- package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
- package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
- package/packages/client/dist/commands/qa-scan-capture.js +663 -5
- package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
- package/packages/client/dist/commands/qa-scan-review.js +826 -5
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
- package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
- package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
- package/packages/client/dist/commands/refactor-characterization.js +671 -21
- package/packages/client/dist/commands/refactor-commit.js +399 -13
- package/packages/client/dist/commands/refactor-control.js +142 -3
- package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
- package/packages/client/dist/commands/refactor-review.js +283 -5
- package/packages/client/dist/commands/refactor-root.js +588 -7
- package/packages/client/dist/commands/refresh-memory.js +156 -3
- package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
- package/packages/client/dist/commands/story-checkpoint.js +171 -1
- package/packages/client/dist/config/profile-directory.js +13 -1
- package/packages/client/dist/config/resolver-selection.js +278 -1
- package/packages/client/dist/config/secure-config.js +281 -12
- package/packages/client/dist/config.js +223 -2
- package/packages/client/dist/constants.js +25 -1
- package/packages/client/dist/context/context-collector.js +222 -2
- package/packages/client/dist/context/context-sanitizer.js +145 -1
- package/packages/client/dist/continuity/autonomous-governance.js +285 -1
- package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
- package/packages/client/dist/continuity/invoke-hooks.js +129 -1
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
- package/packages/client/dist/continuity/native-agent-store.js +69 -1
- package/packages/client/dist/continuity/native-agent-types.js +90 -1
- package/packages/client/dist/continuity/outbox.js +117 -1
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
- package/packages/client/dist/continuity/sqlite-store.js +2536 -81
- package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
- package/packages/client/dist/errors/error-messages.js +228 -2
- package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
- package/packages/client/dist/evidence/collector.js +347 -1
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +2 -0
- package/packages/client/dist/fs/atomic-state-replace.js +152 -1
- package/packages/client/dist/generated/command-registry.generated.js +4076 -1
- package/packages/client/dist/generated/public-projections.generated.js +149 -1
- package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
- package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
- package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
- package/packages/client/dist/git/git-lifecycle-store.js +1938 -9
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
- package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
- package/packages/client/dist/git/legacy-story-pr.js +3538 -70
- package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
- package/packages/client/dist/i18n/first-run.js +60 -2
- package/packages/client/dist/index.js +47 -1
- package/packages/client/dist/invocation/invoke-input.js +24 -1
- package/packages/client/dist/license/index.js +5 -1
- package/packages/client/dist/license/license-client.js +304 -1
- package/packages/client/dist/machine/fingerprint.js +163 -2
- package/packages/client/dist/machine/index.js +5 -1
- package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
- package/packages/client/dist/memory/project-memory-writer.js +1007 -40
- package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
- package/packages/client/dist/migrations/project-identity-capability.js +50 -1
- package/packages/client/dist/migrations/project-identity.js +493 -7
- package/packages/client/dist/migrations/state-json.js +344 -4
- package/packages/client/dist/policy/architecture-policy.js +184 -2
- package/packages/client/dist/policy/index.js +7 -1
- package/packages/client/dist/policy/shared-policy-types.js +38 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
- package/packages/client/dist/private-execution/clean-baseline.js +279 -2
- package/packages/client/dist/private-execution/codex-executor.js +574 -4
- package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
- package/packages/client/dist/private-execution/delivery.js +410 -13
- package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
- package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
- package/packages/client/dist/private-execution/implementation-validation.js +106 -2
- package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
- package/packages/client/dist/private-execution/isolated-session.js +725 -5
- package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
- package/packages/client/dist/private-execution/pact-test-source.js +258 -1
- package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
- package/packages/client/dist/private-execution/pact-write-set.js +172 -1
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
- package/packages/client/dist/private-execution/private-report-store.js +42 -1
- package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
- package/packages/client/dist/private-execution/property-test-choices.js +147 -3
- package/packages/client/dist/private-execution/property-test-plan.js +271 -7
- package/packages/client/dist/private-execution/property-test-review.js +258 -4
- package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
- package/packages/client/dist/private-execution/property-test-source.js +139 -1
- package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
- package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
- package/packages/client/dist/private-execution/root-memory-request.js +29 -1
- package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
- package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
- package/packages/client/dist/private-execution/session-lease.js +63 -1
- package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
- package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
- package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
- package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
- package/packages/client/dist/private-execution/ux-write-set.js +191 -2
- package/packages/client/dist/resilience/circuit-breaker.js +170 -1
- package/packages/client/dist/resilience/degradation-manager.js +164 -1
- package/packages/client/dist/resilience/freshness-indicator.js +100 -1
- package/packages/client/dist/resilience/index.js +8 -1
- package/packages/client/dist/resilience/recovery-detector.js +74 -1
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.js +255 -8
- package/packages/client/dist/resolvers/remote-resolver.js +233 -1
- package/packages/client/dist/runner/binary.js +404 -1
- package/packages/client/dist/runner/cli.js +1571 -17
- package/packages/client/dist/runner/headless/adapters.js +49 -1
- package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
- package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
- package/packages/client/dist/runner/headless/managed-profile.js +132 -1
- package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
- package/packages/client/dist/runner/headless/resolver.js +231 -1
- package/packages/client/dist/runner/headless/run-yolo.js +885 -1
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +4 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
- package/packages/client/dist/runner/headless/types.js +574 -2
- package/packages/client/dist/runner/local-console.js +396 -13
- package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
- package/packages/client/dist/runner/project-identity.js +106 -1
- package/packages/client/dist/runner/scheduler.js +995 -7
- package/packages/client/dist/runner-cli.js +18 -2
- package/packages/client/dist/state/invocation-shape.js +13 -1
- package/packages/client/dist/state/project-state-snapshot.js +1677 -1
- package/packages/client/dist/state/state-json-repair.js +785 -3
- package/packages/client/dist/state/structural-dag-projection.js +118 -1
- package/packages/client/dist/telemetry/index.js +5 -1
- package/packages/client/dist/telemetry/offline-queue.js +131 -1
- package/packages/client/dist/tier/index.js +5 -1
- package/packages/client/dist/tier/tier-aware-client.js +405 -1
- package/packages/client/dist/types/index.js +38 -1
- package/packages/client/dist/version.js +27 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
- package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
- package/packages/client/dist/yolo/spec-envelope-collector.js +185 -5
- package/packages/client/dist/yoloop/discovery-hook.js +300 -2
- package/packages/client/dist/yoloop/index.js +8 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
- package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
- package/packages/client/dist/yoloop/target-normalization.js +180 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1723 -1
- package/packages/shared/dist/compatibility/index.js +476 -0
- package/packages/shared/dist/yoloop/release-blocked.js +653 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +3 -2
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +5 -5
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +8 -9
- package/targets-stubs/codex/AGENTS.md +3 -3
- package/targets-stubs/cursor/agent.md +4 -3
- package/targets-stubs/gemini-cli/agent.md +5 -4
- package/targets-stubs/opencode/neocortex-root.md +3 -3
- package/targets-stubs/vscode/neocortex.agent.md +5 -4
|
@@ -1 +1,62 @@
|
|
|
1
|
-
import{YOLO_INVOKE_ENVELOPE_CAPABILITY
|
|
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,5 +1,185 @@
|
|
|
1
|
-
import{execFileSync
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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,2 +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 +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';
|