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