decision-os-mcp 0.3.2 → 0.5.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/AGENTS.md +101 -0
- package/README.md +38 -3
- package/dist/core/hierarchical-storage.d.ts +128 -0
- package/dist/core/hierarchical-storage.d.ts.map +1 -0
- package/dist/core/hierarchical-storage.js +368 -0
- package/dist/core/hierarchical-storage.js.map +1 -0
- package/dist/core/schemas.d.ts +821 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +289 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/services.d.ts +54 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +54 -0
- package/dist/core/services.js.map +1 -0
- package/dist/core/storage.d.ts +119 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +584 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -862
- package/dist/index.js.map +1 -1
- package/dist/integrations/litellm/callback-handler.d.ts +44 -0
- package/dist/integrations/litellm/callback-handler.d.ts.map +1 -0
- package/dist/integrations/litellm/callback-handler.js +76 -0
- package/dist/integrations/litellm/callback-handler.js.map +1 -0
- package/dist/integrations/litellm/cli.d.ts +23 -0
- package/dist/integrations/litellm/cli.d.ts.map +1 -0
- package/dist/integrations/litellm/cli.js +122 -0
- package/dist/integrations/litellm/cli.js.map +1 -0
- package/dist/observer/detect.d.ts +36 -0
- package/dist/observer/detect.d.ts.map +1 -0
- package/dist/observer/detect.js +183 -0
- package/dist/observer/detect.js.map +1 -0
- package/dist/observer/engine.d.ts +22 -0
- package/dist/observer/engine.d.ts.map +1 -0
- package/dist/observer/engine.js +52 -0
- package/dist/observer/engine.js.map +1 -0
- package/dist/observer/orchestrator.d.ts +45 -0
- package/dist/observer/orchestrator.d.ts.map +1 -0
- package/dist/observer/orchestrator.js +65 -0
- package/dist/observer/orchestrator.js.map +1 -0
- package/dist/observer/persistence.d.ts +14 -0
- package/dist/observer/persistence.d.ts.map +1 -0
- package/dist/observer/persistence.js +41 -0
- package/dist/observer/persistence.js.map +1 -0
- package/dist/observer/projections.d.ts +23 -0
- package/dist/observer/projections.d.ts.map +1 -0
- package/dist/observer/projections.js +96 -0
- package/dist/observer/projections.js.map +1 -0
- package/dist/observer/state.d.ts +11 -0
- package/dist/observer/state.d.ts.map +1 -0
- package/dist/observer/state.js +64 -0
- package/dist/observer/state.js.map +1 -0
- package/dist/server/format-table.d.ts +16 -0
- package/dist/server/format-table.d.ts.map +1 -0
- package/dist/server/format-table.js +40 -0
- package/dist/server/format-table.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +873 -0
- package/dist/server/index.js.map +1 -0
- package/integrations/__init__.py +0 -0
- package/integrations/litellm/__init__.py +0 -0
- package/integrations/litellm/config.yaml +17 -0
- package/integrations/litellm/decision_os_callback.py +192 -0
- package/integrations/litellm/env.example +30 -0
- package/integrations/litellm/litellm_proxy_entry.py +58 -0
- package/integrations/litellm/start.sh +58 -0
- package/package.json +18 -2
- package/templates/AGENTS.md +38 -0
- package/templates/claude-desktop-mcp-config.json +11 -0
- package/src/hierarchical-storage.ts +0 -505
- package/src/index.ts +0 -932
- package/src/schemas.ts +0 -296
- package/src/storage.ts +0 -829
- package/test/hierarchical-storage.test.ts +0 -223
- package/test/storage.test.ts +0 -551
- package/tsconfig.json +0 -19
package/AGENTS.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Decision OS MCP
|
|
2
|
+
|
|
3
|
+
MCP server for Decision OS — an LLM-native decision tracking and learning system. TypeScript, Node.js 18+, ES modules, `@modelcontextprotocol/sdk`, YAML storage, Zod validation.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install # Install dependencies
|
|
9
|
+
npm run build # Compile TypeScript (tsc)
|
|
10
|
+
npm run dev # Watch mode (tsc --watch)
|
|
11
|
+
npm test # Run all tests (vitest run)
|
|
12
|
+
npm run test:watch # Watch tests (vitest)
|
|
13
|
+
npm start # Run server (set DECISION_OS_PATH first)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run `npm run build && npm test` before committing. All tests must pass.
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
src/
|
|
22
|
+
├── index.ts # Public API surface (re-exports)
|
|
23
|
+
├── core/
|
|
24
|
+
│ ├── schemas.ts # Zod schemas for all types (Decision OS + Observer)
|
|
25
|
+
│ ├── storage.ts # Single-scope storage engine (YAML read/write)
|
|
26
|
+
│ ├── hierarchical-storage.ts # Multi-scope storage (PROJECT + GLOBAL cascading)
|
|
27
|
+
│ └── services.ts # Thin service layer over storage (used by observer)
|
|
28
|
+
├── observer/
|
|
29
|
+
│ ├── engine.ts # observe(state, newTurns) → events + actions
|
|
30
|
+
│ ├── state.ts # ObserverMetaState creation + event reducer
|
|
31
|
+
│ ├── projections.ts # Map observer actions → Decision OS core calls + feedback
|
|
32
|
+
│ ├── orchestrator.ts # Full cycle: detect → project → feedback → persist
|
|
33
|
+
│ └── persistence.ts # Save/load observer sessions as JSON
|
|
34
|
+
├── integrations/
|
|
35
|
+
│ └── litellm/
|
|
36
|
+
│ └── callback-handler.ts # LiteLLM callback → observer orchestrator
|
|
37
|
+
└── server/
|
|
38
|
+
└── index.ts # MCP server entry, tool definitions, request handler
|
|
39
|
+
test/
|
|
40
|
+
├── storage.test.ts
|
|
41
|
+
├── hierarchical-storage.test.ts
|
|
42
|
+
└── observer.test.ts
|
|
43
|
+
templates/ # Setup templates for consumers
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Core
|
|
47
|
+
- Entry point registers 13 MCP tools via `@modelcontextprotocol/sdk`
|
|
48
|
+
- All tool input validation uses Zod schemas from `core/schemas.ts`
|
|
49
|
+
- Storage is YAML-based, file-system only, no network calls
|
|
50
|
+
- Hierarchical storage merges `~/.decision-os/` (GLOBAL) with project-level `.decision-os/` (PROJECT)
|
|
51
|
+
- PROJECT scope wins over GLOBAL on conflicts
|
|
52
|
+
- `DecisionOSService` provides a programmatic API over storage (no MCP dependency)
|
|
53
|
+
|
|
54
|
+
### Observer
|
|
55
|
+
- Session-local meta-state tracks task stage, case lifecycle, and pressure signals
|
|
56
|
+
- `observe()` processes new conversation turns incrementally (not full conversation)
|
|
57
|
+
- V1 uses heuristic detection for 3 transitions: TASK_START, PRESSURE_DETECTED, COMPLETION_SIGNAL
|
|
58
|
+
- `projectActions()` maps observer actions into Decision OS core calls via the service layer
|
|
59
|
+
- `toFeedbackEvents()` converts projection results back into state events (CASE_OPENED, CASE_CLOSED)
|
|
60
|
+
- `runCycle()` composes the full loop: detect → project → feedback → state update
|
|
61
|
+
- `ObserverOrchestrator` wraps `runCycle()` with persistence load/save
|
|
62
|
+
- Observer state persists under `.decision-os/observer/sessions/` as JSON
|
|
63
|
+
- **Core never imports from observer.** Observer calls core through the service layer.
|
|
64
|
+
|
|
65
|
+
### LiteLLM Integration
|
|
66
|
+
- `LiteLLMCallbackHandler` converts LiteLLM callback events into ObserverTurns
|
|
67
|
+
- Normalizes roles (user/assistant/tool), skips system messages
|
|
68
|
+
- Feeds turns through the ObserverOrchestrator on each successful callback
|
|
69
|
+
|
|
70
|
+
## Conventions
|
|
71
|
+
|
|
72
|
+
- ES modules (`"type": "module"` in package.json) — use `.js` extensions in imports
|
|
73
|
+
- Strict TypeScript (`strict: true`, target ES2022, NodeNext module resolution) — no `any`, no `@ts-ignore`
|
|
74
|
+
- Tool definitions in `TOOLS` array follow MCP SDK `Tool` type with `annotations`
|
|
75
|
+
- All tool handlers live in the `switch` block inside `CallToolRequestSchema` handler
|
|
76
|
+
- Zod for all input validation; errors formatted as `path: message`
|
|
77
|
+
- Error handling: `ZodError` → formatted validation message; `Error` → `.message`; else → `String(error)`
|
|
78
|
+
- Regret values accept both number (0-3) and string ("0"-"3") via `z.preprocess`
|
|
79
|
+
- Foundation IDs: `F-NNNN` (project), `GF-NNNN` (global)
|
|
80
|
+
- Pressure event IDs: `PE-NNNN`
|
|
81
|
+
- Case IDs: `NNNN-slug-title`
|
|
82
|
+
|
|
83
|
+
## Testing
|
|
84
|
+
|
|
85
|
+
- Tests use Vitest with temp directories for isolation
|
|
86
|
+
- Test files: `test/storage.test.ts`, `test/hierarchical-storage.test.ts`, `test/observer.test.ts`
|
|
87
|
+
|
|
88
|
+
## Adding a New Tool
|
|
89
|
+
|
|
90
|
+
1. Add input schema to `core/schemas.ts`
|
|
91
|
+
2. Add `Tool` definition to `TOOLS` array in `server/index.ts` (include `annotations`)
|
|
92
|
+
3. Add handler case to the `switch` block in the `CallToolRequestSchema` handler
|
|
93
|
+
4. Add tests in `test/`
|
|
94
|
+
5. Update tool table in `README.md`
|
|
95
|
+
|
|
96
|
+
## PR Guidelines
|
|
97
|
+
|
|
98
|
+
- Title format: descriptive summary of the change
|
|
99
|
+
- Run `npm run build && npm test` before opening a PR
|
|
100
|
+
- Include test coverage for new tools or storage logic
|
|
101
|
+
- Update `README.md` tool table if adding/removing tools
|
package/README.md
CHANGED
|
@@ -32,7 +32,9 @@ cp -r templates/.decision-os /path/to/your-project/
|
|
|
32
32
|
|
|
33
33
|
Edit `config.yaml` with your project name.
|
|
34
34
|
|
|
35
|
-
### 3. Configure
|
|
35
|
+
### 3. Configure Your Agent
|
|
36
|
+
|
|
37
|
+
#### Cursor
|
|
36
38
|
|
|
37
39
|
Add to your project's `.cursor/mcp.json`:
|
|
38
40
|
|
|
@@ -56,6 +58,35 @@ Copy the Cursor rules template:
|
|
|
56
58
|
cp templates/.cursor/rules/decision-os.mdc /path/to/your-project/.cursor/rules/
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
#### Claude Code / Claude Desktop
|
|
62
|
+
|
|
63
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"decision-os": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "decision-os-mcp"],
|
|
71
|
+
"env": {
|
|
72
|
+
"DECISION_OS_PATH": "/absolute/path/to/your-project/.decision-os"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Copy the agent instructions template (includes a `.claude/rules/` symlink to AGENTS.md):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cp templates/AGENTS.md /path/to/your-project/
|
|
83
|
+
cp -R templates/.claude /path/to/your-project/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> **Note:** Claude Desktop requires absolute paths in `DECISION_OS_PATH` (no `${workspaceFolder}`).
|
|
87
|
+
|
|
88
|
+
[AGENTS.md](https://agents.md) is an open standard supported by 20+ coding agents including OpenAI Codex, Google Jules, Claude Code, Cursor, Aider, Zed, Warp, VS Code, and more. The `.claude/rules/` symlink lets Claude Code pick it up automatically too — one file, every agent.
|
|
89
|
+
|
|
59
90
|
## Tools
|
|
60
91
|
|
|
61
92
|
| Tool | Description |
|
|
@@ -178,10 +209,14 @@ your-project/
|
|
|
178
209
|
│ │ └── ...
|
|
179
210
|
│ └── defaults/
|
|
180
211
|
│ └── foundations.yaml # F-prefixed project learnings
|
|
181
|
-
├── .cursor/
|
|
212
|
+
├── .cursor/ # Cursor setup
|
|
182
213
|
│ ├── mcp.json # MCP server config
|
|
183
214
|
│ └── rules/
|
|
184
|
-
│ └── decision-os.mdc # LLM instructions
|
|
215
|
+
│ └── decision-os.mdc # LLM instructions (Cursor format)
|
|
216
|
+
├── .claude/ # Claude Code setup
|
|
217
|
+
│ └── rules/
|
|
218
|
+
│ └── decision-os.md # -> symlink to ../../AGENTS.md
|
|
219
|
+
├── AGENTS.md # Agent instructions (Claude Code, Codex, Jules, etc.)
|
|
185
220
|
└── src/
|
|
186
221
|
```
|
|
187
222
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DecisionOSStorage } from "./storage.js";
|
|
2
|
+
import type { Foundation, Case, PressureEvent, ProjectConfig } from "./schemas.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extended Foundation type with source layer information for conflict visibility
|
|
5
|
+
*/
|
|
6
|
+
export interface FoundationWithSource extends Foundation {
|
|
7
|
+
_source_layer: string;
|
|
8
|
+
_source_scope: "GLOBAL" | "PROJECT";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Conflict information when foundations overlap across scopes
|
|
12
|
+
*/
|
|
13
|
+
export interface FoundationConflict {
|
|
14
|
+
title: string;
|
|
15
|
+
global_foundation: Foundation;
|
|
16
|
+
project_foundation: Foundation;
|
|
17
|
+
recommendation: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Hierarchical storage layer for Decision OS.
|
|
21
|
+
* Manages GLOBAL → PROJECT cascading scope model.
|
|
22
|
+
*
|
|
23
|
+
* Resolution order: PROJECT wins over GLOBAL for conflicts.
|
|
24
|
+
* Global foundations are recommendations, not rules.
|
|
25
|
+
*/
|
|
26
|
+
export declare class HierarchicalDecisionOSStorage {
|
|
27
|
+
private layers;
|
|
28
|
+
private projectLayer;
|
|
29
|
+
private globalLayer;
|
|
30
|
+
constructor(workspacePath: string);
|
|
31
|
+
/**
|
|
32
|
+
* Discover all .decision-os layers by walking up from workspace path.
|
|
33
|
+
* Returns [nearest, ..., global] order.
|
|
34
|
+
*/
|
|
35
|
+
private discoverLayers;
|
|
36
|
+
/**
|
|
37
|
+
* Initialize all layers
|
|
38
|
+
*/
|
|
39
|
+
initialize(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Get the base path of the project layer
|
|
42
|
+
*/
|
|
43
|
+
getProjectPath(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Get the base path of the global layer (if exists)
|
|
46
|
+
*/
|
|
47
|
+
getGlobalPath(): string | null;
|
|
48
|
+
getConfig(): Promise<ProjectConfig>;
|
|
49
|
+
updateConfig(config: Partial<ProjectConfig>): Promise<void>;
|
|
50
|
+
getActiveCase(): string | null;
|
|
51
|
+
setActiveCase(caseId: string | null): Promise<void>;
|
|
52
|
+
listCases(): Promise<Case[]>;
|
|
53
|
+
getCase(caseId: string): Promise<Case | null>;
|
|
54
|
+
createCase(input: Parameters<DecisionOSStorage["createCase"]>[0]): Promise<Case>;
|
|
55
|
+
updateCase(caseId: string, updates: Partial<Case>): Promise<Case>;
|
|
56
|
+
closeCase(caseId: string, outcome: Parameters<DecisionOSStorage["closeCase"]>[1]): Promise<{
|
|
57
|
+
case: Case;
|
|
58
|
+
forgotten: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
getPressureEvents(caseId: string): Promise<PressureEvent[]>;
|
|
61
|
+
logPressure(input: Parameters<DecisionOSStorage["logPressure"]>[0]): Promise<PressureEvent>;
|
|
62
|
+
searchPressures(query: string): Promise<PressureEvent[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Get merged foundations from all layers.
|
|
65
|
+
* Project foundations take precedence over global on title conflict.
|
|
66
|
+
* Returns foundations annotated with source information.
|
|
67
|
+
*/
|
|
68
|
+
getFoundations(filters?: {
|
|
69
|
+
context_tags?: string[];
|
|
70
|
+
min_confidence?: number;
|
|
71
|
+
}): Promise<FoundationWithSource[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Detect conflicts where project and global have foundations with same title
|
|
74
|
+
* or overlapping context_tags.
|
|
75
|
+
*/
|
|
76
|
+
detectConflicts(): Promise<FoundationConflict[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Promote pressure events to a foundation in the specified scope.
|
|
79
|
+
*/
|
|
80
|
+
promoteToFoundation(input: {
|
|
81
|
+
title: string;
|
|
82
|
+
default_behavior: string;
|
|
83
|
+
context_tags: string[];
|
|
84
|
+
counter_contexts?: string[];
|
|
85
|
+
source_pressures: string[];
|
|
86
|
+
exit_criteria?: string;
|
|
87
|
+
scope?: "GLOBAL" | "PROJECT";
|
|
88
|
+
origin_project?: string;
|
|
89
|
+
}): Promise<Foundation>;
|
|
90
|
+
/**
|
|
91
|
+
* Elevate a project foundation to global scope.
|
|
92
|
+
* Creates a new foundation in global with GF- prefix.
|
|
93
|
+
*/
|
|
94
|
+
elevateFoundation(input: {
|
|
95
|
+
foundation_id: string;
|
|
96
|
+
reason?: string;
|
|
97
|
+
}): Promise<Foundation>;
|
|
98
|
+
/**
|
|
99
|
+
* Cross-validate that a global foundation applies in the current project.
|
|
100
|
+
* Increases confidence and adds project to validated_in list.
|
|
101
|
+
*/
|
|
102
|
+
validateFoundation(input: {
|
|
103
|
+
foundation_id: string;
|
|
104
|
+
validation_notes?: string;
|
|
105
|
+
}): Promise<Foundation>;
|
|
106
|
+
suggestReview(): ReturnType<DecisionOSStorage["suggestReview"]>;
|
|
107
|
+
checkPolicy(signals: Parameters<DecisionOSStorage["checkPolicy"]>[0]): ReturnType<DecisionOSStorage["checkPolicy"]>;
|
|
108
|
+
getContext(): Promise<{
|
|
109
|
+
project: string;
|
|
110
|
+
active_case: Case | null;
|
|
111
|
+
recent_pressures: PressureEvent[];
|
|
112
|
+
relevant_foundations: FoundationWithSource[];
|
|
113
|
+
conflicts: FoundationConflict[];
|
|
114
|
+
layers: string[];
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Rank foundations by relevance to the active case.
|
|
118
|
+
* Foundations matching the case's affected_surface or touched_areas
|
|
119
|
+
* are marked "directly_relevant" and sorted first.
|
|
120
|
+
*/
|
|
121
|
+
private rankFoundationsByRelevance;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a hierarchical storage instance, discovering layers from workspace path.
|
|
125
|
+
* Falls back to creating project-only storage if no hierarchy found.
|
|
126
|
+
*/
|
|
127
|
+
export declare function createHierarchicalStorage(workspacePath: string): HierarchicalDecisionOSStorage;
|
|
128
|
+
//# sourceMappingURL=hierarchical-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hierarchical-storage.d.ts","sourceRoot":"","sources":["../../src/core/hierarchical-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { DecisionOSStorage } from "./storage.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hierarchical storage layer for Decision OS.
|
|
7
|
+
* Manages GLOBAL → PROJECT cascading scope model.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order: PROJECT wins over GLOBAL for conflicts.
|
|
10
|
+
* Global foundations are recommendations, not rules.
|
|
11
|
+
*/
|
|
12
|
+
export class HierarchicalDecisionOSStorage {
|
|
13
|
+
layers = []; // [project, global] - nearest first
|
|
14
|
+
projectLayer;
|
|
15
|
+
globalLayer = null;
|
|
16
|
+
constructor(workspacePath) {
|
|
17
|
+
this.layers = this.discoverLayers(workspacePath);
|
|
18
|
+
if (this.layers.length === 0) {
|
|
19
|
+
throw new Error(`No .decision-os found starting from ${workspacePath}`);
|
|
20
|
+
}
|
|
21
|
+
this.projectLayer = this.layers[0];
|
|
22
|
+
this.globalLayer = this.layers.length > 1 ? this.layers[this.layers.length - 1] : null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Discover all .decision-os layers by walking up from workspace path.
|
|
26
|
+
* Returns [nearest, ..., global] order.
|
|
27
|
+
*/
|
|
28
|
+
discoverLayers(startPath) {
|
|
29
|
+
const layers = [];
|
|
30
|
+
const seenPaths = new Set();
|
|
31
|
+
// First, check if startPath itself is or contains .decision-os
|
|
32
|
+
let currentPath = startPath;
|
|
33
|
+
// If startPath ends with .decision-os, use its parent as start
|
|
34
|
+
if (currentPath.endsWith(".decision-os")) {
|
|
35
|
+
currentPath = dirname(currentPath);
|
|
36
|
+
}
|
|
37
|
+
// Walk up directory tree looking for .decision-os folders
|
|
38
|
+
while (currentPath !== "/" && currentPath !== dirname(currentPath)) {
|
|
39
|
+
const dosPath = join(currentPath, ".decision-os");
|
|
40
|
+
if (existsSync(dosPath) && !seenPaths.has(dosPath)) {
|
|
41
|
+
seenPaths.add(dosPath);
|
|
42
|
+
layers.push(new DecisionOSStorage(dosPath));
|
|
43
|
+
}
|
|
44
|
+
currentPath = dirname(currentPath);
|
|
45
|
+
}
|
|
46
|
+
// Always include global ~/.decision-os if it exists
|
|
47
|
+
const globalPath = join(homedir(), ".decision-os");
|
|
48
|
+
if (existsSync(globalPath) && !seenPaths.has(globalPath)) {
|
|
49
|
+
seenPaths.add(globalPath);
|
|
50
|
+
layers.push(new DecisionOSStorage(globalPath));
|
|
51
|
+
}
|
|
52
|
+
return layers;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Initialize all layers
|
|
56
|
+
*/
|
|
57
|
+
async initialize() {
|
|
58
|
+
for (const layer of this.layers) {
|
|
59
|
+
await layer.initialize();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the base path of the project layer
|
|
64
|
+
*/
|
|
65
|
+
getProjectPath() {
|
|
66
|
+
return this.projectLayer.basePath;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the base path of the global layer (if exists)
|
|
70
|
+
*/
|
|
71
|
+
getGlobalPath() {
|
|
72
|
+
return this.globalLayer ? this.globalLayer.basePath : null;
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// CONFIG
|
|
76
|
+
// ============================================================================
|
|
77
|
+
async getConfig() {
|
|
78
|
+
return this.projectLayer.getConfig();
|
|
79
|
+
}
|
|
80
|
+
async updateConfig(config) {
|
|
81
|
+
return this.projectLayer.updateConfig(config);
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// ACTIVE CASE (project-local)
|
|
85
|
+
// ============================================================================
|
|
86
|
+
getActiveCase() {
|
|
87
|
+
return this.projectLayer.getActiveCase();
|
|
88
|
+
}
|
|
89
|
+
async setActiveCase(caseId) {
|
|
90
|
+
await this.projectLayer.setActiveCase(caseId);
|
|
91
|
+
}
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// CASES (project-local)
|
|
94
|
+
// ============================================================================
|
|
95
|
+
async listCases() {
|
|
96
|
+
return this.projectLayer.listCases();
|
|
97
|
+
}
|
|
98
|
+
async getCase(caseId) {
|
|
99
|
+
return this.projectLayer.getCase(caseId);
|
|
100
|
+
}
|
|
101
|
+
async createCase(input) {
|
|
102
|
+
return this.projectLayer.createCase(input);
|
|
103
|
+
}
|
|
104
|
+
async updateCase(caseId, updates) {
|
|
105
|
+
return this.projectLayer.updateCase(caseId, updates);
|
|
106
|
+
}
|
|
107
|
+
async closeCase(caseId, outcome) {
|
|
108
|
+
return this.projectLayer.closeCase(caseId, outcome);
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// PRESSURE EVENTS (project-local)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
async getPressureEvents(caseId) {
|
|
114
|
+
return this.projectLayer.getPressureEvents(caseId);
|
|
115
|
+
}
|
|
116
|
+
async logPressure(input) {
|
|
117
|
+
return this.projectLayer.logPressure(input);
|
|
118
|
+
}
|
|
119
|
+
async searchPressures(query) {
|
|
120
|
+
// Search only in project layer (per user's answer to Q3)
|
|
121
|
+
return this.projectLayer.searchPressures(query);
|
|
122
|
+
}
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// FOUNDATIONS (merged across layers)
|
|
125
|
+
// ============================================================================
|
|
126
|
+
/**
|
|
127
|
+
* Get merged foundations from all layers.
|
|
128
|
+
* Project foundations take precedence over global on title conflict.
|
|
129
|
+
* Returns foundations annotated with source information.
|
|
130
|
+
*/
|
|
131
|
+
async getFoundations(filters) {
|
|
132
|
+
const seen = new Map(); // Track by title
|
|
133
|
+
const merged = [];
|
|
134
|
+
for (const layer of this.layers) {
|
|
135
|
+
const foundations = await layer.getFoundations(filters);
|
|
136
|
+
const layerPath = layer.basePath;
|
|
137
|
+
const isGlobal = layerPath === join(homedir(), ".decision-os");
|
|
138
|
+
for (const f of foundations) {
|
|
139
|
+
const annotated = {
|
|
140
|
+
...f,
|
|
141
|
+
_source_layer: layerPath,
|
|
142
|
+
_source_scope: isGlobal ? "GLOBAL" : "PROJECT",
|
|
143
|
+
};
|
|
144
|
+
if (!seen.has(f.title)) {
|
|
145
|
+
// First occurrence (nearest layer) wins
|
|
146
|
+
seen.set(f.title, annotated);
|
|
147
|
+
merged.push(annotated);
|
|
148
|
+
}
|
|
149
|
+
// If already seen, project wins over global (first layer is project)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return merged;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Detect conflicts where project and global have foundations with same title
|
|
156
|
+
* or overlapping context_tags.
|
|
157
|
+
*/
|
|
158
|
+
async detectConflicts() {
|
|
159
|
+
if (!this.globalLayer)
|
|
160
|
+
return [];
|
|
161
|
+
const projectFoundations = await this.projectLayer.getFoundations();
|
|
162
|
+
const globalFoundations = await this.globalLayer.getFoundations();
|
|
163
|
+
const conflicts = [];
|
|
164
|
+
for (const pf of projectFoundations) {
|
|
165
|
+
// Check for title match
|
|
166
|
+
const titleMatch = globalFoundations.find((gf) => gf.title.toLowerCase() === pf.title.toLowerCase());
|
|
167
|
+
if (titleMatch) {
|
|
168
|
+
conflicts.push({
|
|
169
|
+
title: pf.title,
|
|
170
|
+
global_foundation: titleMatch,
|
|
171
|
+
project_foundation: pf,
|
|
172
|
+
recommendation: `Project foundation "${pf.title}" shadows global foundation. ` +
|
|
173
|
+
`Project version will be used. Consider if global should be updated or removed.`,
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Check for overlapping context_tags
|
|
178
|
+
for (const gf of globalFoundations) {
|
|
179
|
+
const overlappingTags = pf.context_tags.filter((t) => gf.context_tags.includes(t));
|
|
180
|
+
if (overlappingTags.length > 0 && pf.title !== gf.title) {
|
|
181
|
+
// Different titles but same context - potential friction
|
|
182
|
+
const pfBehavior = pf.default_behavior.toLowerCase();
|
|
183
|
+
const gfBehavior = gf.default_behavior.toLowerCase();
|
|
184
|
+
// Simple heuristic: if behaviors seem contradictory
|
|
185
|
+
const contradictory = (pfBehavior.includes("always") && gfBehavior.includes("never")) ||
|
|
186
|
+
(pfBehavior.includes("never") && gfBehavior.includes("always")) ||
|
|
187
|
+
(pfBehavior.includes("prefer") && gfBehavior.includes("avoid"));
|
|
188
|
+
if (contradictory) {
|
|
189
|
+
conflicts.push({
|
|
190
|
+
title: `${pf.title} vs ${gf.title}`,
|
|
191
|
+
global_foundation: gf,
|
|
192
|
+
project_foundation: pf,
|
|
193
|
+
recommendation: `Potential conflict: Both apply to [${overlappingTags.join(", ")}] ` +
|
|
194
|
+
`but may have contradictory guidance. Review and clarify.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return conflicts;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Promote pressure events to a foundation in the specified scope.
|
|
204
|
+
*/
|
|
205
|
+
async promoteToFoundation(input) {
|
|
206
|
+
const scope = input.scope ?? "PROJECT";
|
|
207
|
+
const config = await this.projectLayer.getConfig();
|
|
208
|
+
const originProject = input.origin_project ?? config.project;
|
|
209
|
+
const targetLayer = scope === "GLOBAL" && this.globalLayer
|
|
210
|
+
? this.globalLayer
|
|
211
|
+
: this.projectLayer;
|
|
212
|
+
// Create foundation with scope and origin metadata
|
|
213
|
+
const foundation = await targetLayer.promoteToFoundation({
|
|
214
|
+
title: input.title,
|
|
215
|
+
default_behavior: input.default_behavior,
|
|
216
|
+
context_tags: input.context_tags,
|
|
217
|
+
counter_contexts: input.counter_contexts,
|
|
218
|
+
source_pressures: input.source_pressures,
|
|
219
|
+
exit_criteria: input.exit_criteria,
|
|
220
|
+
scope,
|
|
221
|
+
origin_project: originProject,
|
|
222
|
+
});
|
|
223
|
+
return foundation;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Elevate a project foundation to global scope.
|
|
227
|
+
* Creates a new foundation in global with GF- prefix.
|
|
228
|
+
*/
|
|
229
|
+
async elevateFoundation(input) {
|
|
230
|
+
if (!this.globalLayer) {
|
|
231
|
+
throw new Error("No global .decision-os found at ~/.decision-os. " +
|
|
232
|
+
"Create it first with: mkdir -p ~/.decision-os/defaults && " +
|
|
233
|
+
"echo 'foundations: []' > ~/.decision-os/defaults/foundations.yaml");
|
|
234
|
+
}
|
|
235
|
+
// Find the foundation in project layer
|
|
236
|
+
const projectFoundations = await this.projectLayer.getFoundations();
|
|
237
|
+
const foundation = projectFoundations.find((f) => f.id === input.foundation_id);
|
|
238
|
+
if (!foundation) {
|
|
239
|
+
throw new Error(`Foundation not found in project: ${input.foundation_id}`);
|
|
240
|
+
}
|
|
241
|
+
const config = await this.projectLayer.getConfig();
|
|
242
|
+
// Create in global with GF- prefix (scope: GLOBAL triggers the prefix)
|
|
243
|
+
const globalFoundation = await this.globalLayer.promoteToFoundation({
|
|
244
|
+
title: foundation.title,
|
|
245
|
+
default_behavior: foundation.default_behavior +
|
|
246
|
+
(input.reason ? `\n\n[Elevated from ${config.project}: ${input.reason}]` : ""),
|
|
247
|
+
context_tags: foundation.context_tags,
|
|
248
|
+
counter_contexts: foundation.counter_contexts,
|
|
249
|
+
source_pressures: foundation.source_pressures,
|
|
250
|
+
exit_criteria: foundation.exit_criteria,
|
|
251
|
+
scope: "GLOBAL",
|
|
252
|
+
origin_project: config.project,
|
|
253
|
+
});
|
|
254
|
+
// Retire the project copy — knowledge now lives in global
|
|
255
|
+
await this.projectLayer.removeFoundation(input.foundation_id);
|
|
256
|
+
console.error(`Retired project foundation ${input.foundation_id} — elevated to ${globalFoundation.id}`);
|
|
257
|
+
return globalFoundation;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Cross-validate that a global foundation applies in the current project.
|
|
261
|
+
* Increases confidence and adds project to validated_in list.
|
|
262
|
+
*/
|
|
263
|
+
async validateFoundation(input) {
|
|
264
|
+
// Find the foundation across all layers
|
|
265
|
+
const allFoundations = await this.getFoundations();
|
|
266
|
+
const foundation = allFoundations.find((f) => f.id === input.foundation_id);
|
|
267
|
+
if (!foundation) {
|
|
268
|
+
throw new Error(`Foundation not found: ${input.foundation_id}`);
|
|
269
|
+
}
|
|
270
|
+
const config = await this.projectLayer.getConfig();
|
|
271
|
+
// Update validated_in
|
|
272
|
+
const validatedIn = [...(foundation.validated_in ?? [])];
|
|
273
|
+
if (!validatedIn.includes(config.project)) {
|
|
274
|
+
validatedIn.push(config.project);
|
|
275
|
+
}
|
|
276
|
+
// Increase confidence if validated in multiple projects (3+ validations = confidence boost)
|
|
277
|
+
let newConfidence = foundation.confidence;
|
|
278
|
+
if (validatedIn.length >= 3 && newConfidence < 3) {
|
|
279
|
+
newConfidence = Math.min(3, newConfidence + 1);
|
|
280
|
+
}
|
|
281
|
+
// Update the foundation in its source layer
|
|
282
|
+
const targetLayer = foundation._source_scope === "GLOBAL" && this.globalLayer
|
|
283
|
+
? this.globalLayer
|
|
284
|
+
: this.projectLayer;
|
|
285
|
+
// Use the updateFoundation method to persist
|
|
286
|
+
const updatedFoundation = await targetLayer.updateFoundation(input.foundation_id, {
|
|
287
|
+
validated_in: validatedIn,
|
|
288
|
+
confidence: newConfidence,
|
|
289
|
+
});
|
|
290
|
+
return updatedFoundation;
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// SUGGEST REVIEW (delegates to project layer)
|
|
294
|
+
// ============================================================================
|
|
295
|
+
async suggestReview() {
|
|
296
|
+
return this.projectLayer.suggestReview();
|
|
297
|
+
}
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// POLICY CHECK (delegates to project layer)
|
|
300
|
+
// ============================================================================
|
|
301
|
+
checkPolicy(signals) {
|
|
302
|
+
return this.projectLayer.checkPolicy(signals);
|
|
303
|
+
}
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// CONTEXT (merged)
|
|
306
|
+
// ============================================================================
|
|
307
|
+
async getContext() {
|
|
308
|
+
const config = await this.getConfig();
|
|
309
|
+
const activeCase = this.getActiveCase()
|
|
310
|
+
? await this.getCase(this.getActiveCase())
|
|
311
|
+
: null;
|
|
312
|
+
let recentPressures = [];
|
|
313
|
+
if (activeCase) {
|
|
314
|
+
recentPressures = await this.getPressureEvents(activeCase.id);
|
|
315
|
+
}
|
|
316
|
+
const allFoundations = await this.getFoundations();
|
|
317
|
+
const active = allFoundations.filter((f) => f.confidence >= 1);
|
|
318
|
+
const conflicts = await this.detectConflicts();
|
|
319
|
+
// Rank foundations by relevance to active case
|
|
320
|
+
const ranked = this.rankFoundationsByRelevance(active, activeCase);
|
|
321
|
+
return {
|
|
322
|
+
project: config.project,
|
|
323
|
+
active_case: activeCase,
|
|
324
|
+
recent_pressures: recentPressures.slice(-5),
|
|
325
|
+
relevant_foundations: ranked,
|
|
326
|
+
conflicts,
|
|
327
|
+
layers: this.layers.map((l) => l["basePath"]),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Rank foundations by relevance to the active case.
|
|
332
|
+
* Foundations matching the case's affected_surface or touched_areas
|
|
333
|
+
* are marked "directly_relevant" and sorted first.
|
|
334
|
+
*/
|
|
335
|
+
rankFoundationsByRelevance(foundations, activeCase) {
|
|
336
|
+
if (!activeCase)
|
|
337
|
+
return foundations;
|
|
338
|
+
// Collect case context tags from signals and touched_areas
|
|
339
|
+
const caseTags = new Set();
|
|
340
|
+
const surfaces = activeCase.signals?.context?.affected_surface ?? [];
|
|
341
|
+
for (const s of surfaces)
|
|
342
|
+
caseTags.add(s.toUpperCase());
|
|
343
|
+
const areas = activeCase.context?.touched_areas ?? [];
|
|
344
|
+
for (const a of areas)
|
|
345
|
+
caseTags.add(a.toUpperCase());
|
|
346
|
+
if (caseTags.size === 0)
|
|
347
|
+
return foundations;
|
|
348
|
+
// Score each foundation by tag overlap
|
|
349
|
+
const scored = foundations.map((f) => {
|
|
350
|
+
const overlap = f.context_tags.filter((t) => caseTags.has(t.toUpperCase())).length;
|
|
351
|
+
return { foundation: f, overlap };
|
|
352
|
+
});
|
|
353
|
+
// Sort: directly relevant first, then general
|
|
354
|
+
scored.sort((a, b) => b.overlap - a.overlap);
|
|
355
|
+
return scored.map(({ foundation, overlap }) => ({
|
|
356
|
+
...foundation,
|
|
357
|
+
_relevance: overlap > 0 ? "directly_relevant" : "general",
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Create a hierarchical storage instance, discovering layers from workspace path.
|
|
363
|
+
* Falls back to creating project-only storage if no hierarchy found.
|
|
364
|
+
*/
|
|
365
|
+
export function createHierarchicalStorage(workspacePath) {
|
|
366
|
+
return new HierarchicalDecisionOSStorage(workspacePath);
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=hierarchical-storage.js.map
|