@soleri/core 9.8.0 → 9.9.0
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/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +11 -2
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/paths.js.map +1 -1
- package/dist/planning/gap-patterns.d.ts.map +1 -1
- package/dist/planning/gap-patterns.js +4 -1
- package/dist/planning/gap-patterns.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +14 -6
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +52 -4
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +12 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +76 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/vault/vault-markdown-sync.d.ts +5 -2
- package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
- package/dist/vault/vault-markdown-sync.js +13 -2
- package/dist/vault/vault-markdown-sync.js.map +1 -1
- package/dist/workflows/index.d.ts +6 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +5 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/workflow-loader.d.ts +83 -0
- package/dist/workflows/workflow-loader.d.ts.map +1 -0
- package/dist/workflows/workflow-loader.js +207 -0
- package/dist/workflows/workflow-loader.js.map +1 -0
- package/package.json +1 -1
- package/src/brain/intelligence.ts +15 -2
- package/src/brain/types.ts +1 -0
- package/src/enforcement/adapters/opencode.test.ts +4 -2
- package/src/index.ts +19 -0
- package/src/paths.ts +5 -0
- package/src/planning/gap-patterns.ts +7 -3
- package/src/runtime/capture-ops.test.ts +58 -1
- package/src/runtime/capture-ops.ts +15 -4
- package/src/runtime/facades/curator-facade.test.ts +87 -9
- package/src/runtime/facades/curator-facade.ts +60 -4
- package/src/runtime/orchestrate-ops.ts +84 -0
- package/src/vault/vault-markdown-sync.test.ts +40 -0
- package/src/vault/vault-markdown-sync.ts +16 -3
- package/src/workflows/index.ts +12 -0
- package/src/workflows/orchestrate-integration.test.ts +166 -0
- package/src/workflows/workflow-loader.test.ts +149 -0
- package/src/workflows/workflow-loader.ts +238 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader — reads agent workflow overrides from the file tree.
|
|
3
|
+
*
|
|
4
|
+
* Each workflow is a folder under `workflows/` containing:
|
|
5
|
+
* - `prompt.md` — system prompt for the workflow (optional)
|
|
6
|
+
* - `gates.yaml` — gate definitions (optional)
|
|
7
|
+
* - `tools.yaml` — tool allowlist (optional)
|
|
8
|
+
*
|
|
9
|
+
* These overrides are merged into the OrchestrationPlan when
|
|
10
|
+
* the detected intent matches a workflow via WORKFLOW_TO_INTENT.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
export declare const WorkflowGateSchema: z.ZodObject<{
|
|
14
|
+
phase: z.ZodString;
|
|
15
|
+
requirement: z.ZodString;
|
|
16
|
+
check: z.ZodString;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
check: string;
|
|
19
|
+
phase: string;
|
|
20
|
+
requirement: string;
|
|
21
|
+
}, {
|
|
22
|
+
check: string;
|
|
23
|
+
phase: string;
|
|
24
|
+
requirement: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare const WorkflowOverrideSchema: z.ZodObject<{
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
29
|
+
gates: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
30
|
+
phase: z.ZodString;
|
|
31
|
+
requirement: z.ZodString;
|
|
32
|
+
check: z.ZodString;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
check: string;
|
|
35
|
+
phase: string;
|
|
36
|
+
requirement: string;
|
|
37
|
+
}, {
|
|
38
|
+
check: string;
|
|
39
|
+
phase: string;
|
|
40
|
+
requirement: string;
|
|
41
|
+
}>, "many">>;
|
|
42
|
+
tools: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
43
|
+
}, "strip", z.ZodTypeAny, {
|
|
44
|
+
name: string;
|
|
45
|
+
tools: string[];
|
|
46
|
+
gates: {
|
|
47
|
+
check: string;
|
|
48
|
+
phase: string;
|
|
49
|
+
requirement: string;
|
|
50
|
+
}[];
|
|
51
|
+
prompt?: string | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
name: string;
|
|
54
|
+
prompt?: string | undefined;
|
|
55
|
+
tools?: string[] | undefined;
|
|
56
|
+
gates?: {
|
|
57
|
+
check: string;
|
|
58
|
+
phase: string;
|
|
59
|
+
requirement: string;
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
}>;
|
|
62
|
+
export type WorkflowGate = z.infer<typeof WorkflowGateSchema>;
|
|
63
|
+
export type WorkflowOverride = z.infer<typeof WorkflowOverrideSchema>;
|
|
64
|
+
/**
|
|
65
|
+
* Maps workflow folder names to intent strings.
|
|
66
|
+
* Used by `getWorkflowForIntent()` to find a matching workflow.
|
|
67
|
+
*/
|
|
68
|
+
export declare const WORKFLOW_TO_INTENT: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Load all workflow overrides from an agent's `workflows/` directory.
|
|
71
|
+
*
|
|
72
|
+
* Returns an empty Map if the directory doesn't exist or can't be read
|
|
73
|
+
* (graceful degradation — no throw).
|
|
74
|
+
*/
|
|
75
|
+
export declare function loadAgentWorkflows(workflowsDir: string): Map<string, WorkflowOverride>;
|
|
76
|
+
/**
|
|
77
|
+
* Find a workflow override that matches the given intent.
|
|
78
|
+
*
|
|
79
|
+
* Uses WORKFLOW_TO_INTENT mapping, optionally overridden by customMapping.
|
|
80
|
+
* Returns null if no matching workflow is found.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getWorkflowForIntent(workflows: Map<string, WorkflowOverride>, intent: string, customMapping?: Record<string, string>): WorkflowOverride | null;
|
|
83
|
+
//# sourceMappingURL=workflow-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-loader.d.ts","sourceRoot":"","sources":["../../src/workflows/workflow-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;EAI7B,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKjC,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAMtE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOrD,CAAC;AAMF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA2DtF;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACxC,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,gBAAgB,GAAG,IAAI,CAWzB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow loader — reads agent workflow overrides from the file tree.
|
|
3
|
+
*
|
|
4
|
+
* Each workflow is a folder under `workflows/` containing:
|
|
5
|
+
* - `prompt.md` — system prompt for the workflow (optional)
|
|
6
|
+
* - `gates.yaml` — gate definitions (optional)
|
|
7
|
+
* - `tools.yaml` — tool allowlist (optional)
|
|
8
|
+
*
|
|
9
|
+
* These overrides are merged into the OrchestrationPlan when
|
|
10
|
+
* the detected intent matches a workflow via WORKFLOW_TO_INTENT.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Schemas
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const WorkflowGateSchema = z.object({
|
|
19
|
+
phase: z.string(),
|
|
20
|
+
requirement: z.string(),
|
|
21
|
+
check: z.string(),
|
|
22
|
+
});
|
|
23
|
+
export const WorkflowOverrideSchema = z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
prompt: z.string().optional(),
|
|
26
|
+
gates: z.array(WorkflowGateSchema).default([]),
|
|
27
|
+
tools: z.array(z.string()).default([]),
|
|
28
|
+
});
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Workflow → Intent mapping
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Maps workflow folder names to intent strings.
|
|
34
|
+
* Used by `getWorkflowForIntent()` to find a matching workflow.
|
|
35
|
+
*/
|
|
36
|
+
export const WORKFLOW_TO_INTENT = {
|
|
37
|
+
'feature-dev': 'BUILD',
|
|
38
|
+
'bug-fix': 'FIX',
|
|
39
|
+
'code-review': 'REVIEW',
|
|
40
|
+
'component-build': 'BUILD',
|
|
41
|
+
'token-migration': 'ENHANCE',
|
|
42
|
+
'a11y-remediation': 'FIX',
|
|
43
|
+
};
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Loader
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Load all workflow overrides from an agent's `workflows/` directory.
|
|
49
|
+
*
|
|
50
|
+
* Returns an empty Map if the directory doesn't exist or can't be read
|
|
51
|
+
* (graceful degradation — no throw).
|
|
52
|
+
*/
|
|
53
|
+
export function loadAgentWorkflows(workflowsDir) {
|
|
54
|
+
const workflows = new Map();
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = fs.readdirSync(workflowsDir);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Directory doesn't exist or can't be read — that's fine
|
|
61
|
+
return workflows;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const fullPath = path.join(workflowsDir, entry);
|
|
65
|
+
let stat;
|
|
66
|
+
try {
|
|
67
|
+
stat = fs.statSync(fullPath);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!stat.isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
const override = { name: entry, gates: [], tools: [] };
|
|
75
|
+
// Read prompt.md
|
|
76
|
+
const promptPath = path.join(fullPath, 'prompt.md');
|
|
77
|
+
try {
|
|
78
|
+
override.prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// No prompt — that's fine
|
|
82
|
+
}
|
|
83
|
+
// Read gates.yaml
|
|
84
|
+
const gatesPath = path.join(fullPath, 'gates.yaml');
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(gatesPath, 'utf-8');
|
|
87
|
+
// Simple YAML parsing for the gates structure
|
|
88
|
+
const gates = parseGatesYaml(raw);
|
|
89
|
+
override.gates = gates;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// No gates — that's fine
|
|
93
|
+
}
|
|
94
|
+
// Read tools.yaml
|
|
95
|
+
const toolsPath = path.join(fullPath, 'tools.yaml');
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(toolsPath, 'utf-8');
|
|
98
|
+
const tools = parseToolsYaml(raw);
|
|
99
|
+
override.tools = tools;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// No tools — that's fine
|
|
103
|
+
}
|
|
104
|
+
// Only store if we got something useful
|
|
105
|
+
if (override.prompt || override.gates.length > 0 || override.tools.length > 0) {
|
|
106
|
+
workflows.set(entry, override);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return workflows;
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Intent matching
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* Find a workflow override that matches the given intent.
|
|
116
|
+
*
|
|
117
|
+
* Uses WORKFLOW_TO_INTENT mapping, optionally overridden by customMapping.
|
|
118
|
+
* Returns null if no matching workflow is found.
|
|
119
|
+
*/
|
|
120
|
+
export function getWorkflowForIntent(workflows, intent, customMapping) {
|
|
121
|
+
const mapping = customMapping ?? WORKFLOW_TO_INTENT;
|
|
122
|
+
const normalizedIntent = intent.toUpperCase();
|
|
123
|
+
for (const [workflowName, mappedIntent] of Object.entries(mapping)) {
|
|
124
|
+
if (mappedIntent.toUpperCase() === normalizedIntent && workflows.has(workflowName)) {
|
|
125
|
+
return workflows.get(workflowName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Minimal YAML parsers (no external dependency)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Parse a simple gates.yaml file. Expected format:
|
|
135
|
+
*
|
|
136
|
+
* ```yaml
|
|
137
|
+
* gates:
|
|
138
|
+
* - phase: brainstorming
|
|
139
|
+
* requirement: Requirements are clear
|
|
140
|
+
* check: user-approval
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
function parseGatesYaml(raw) {
|
|
144
|
+
const gates = [];
|
|
145
|
+
const lines = raw.split('\n');
|
|
146
|
+
let current = null;
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
// Skip empty lines and the root "gates:" key
|
|
150
|
+
if (!trimmed || trimmed === 'gates:')
|
|
151
|
+
continue;
|
|
152
|
+
// New list item
|
|
153
|
+
if (trimmed.startsWith('- ')) {
|
|
154
|
+
if (current && current.phase && current.requirement && current.check) {
|
|
155
|
+
gates.push(current);
|
|
156
|
+
}
|
|
157
|
+
current = {};
|
|
158
|
+
// Parse inline key from "- phase: value"
|
|
159
|
+
const inlineMatch = trimmed.match(/^-\s+(\w+):\s*(.+)$/);
|
|
160
|
+
if (inlineMatch) {
|
|
161
|
+
const [, key, value] = inlineMatch;
|
|
162
|
+
if (key === 'phase' || key === 'requirement' || key === 'check') {
|
|
163
|
+
current[key] = value.trim();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Continuation key: " requirement: value"
|
|
169
|
+
if (current) {
|
|
170
|
+
const kvMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
171
|
+
if (kvMatch) {
|
|
172
|
+
const [, key, value] = kvMatch;
|
|
173
|
+
if (key === 'phase' || key === 'requirement' || key === 'check') {
|
|
174
|
+
current[key] = value.trim();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Flush last entry
|
|
180
|
+
if (current && current.phase && current.requirement && current.check) {
|
|
181
|
+
gates.push(current);
|
|
182
|
+
}
|
|
183
|
+
return gates;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Parse a simple tools.yaml file. Expected format:
|
|
187
|
+
*
|
|
188
|
+
* ```yaml
|
|
189
|
+
* tools:
|
|
190
|
+
* - soleri_vault op:search_intelligent
|
|
191
|
+
* - soleri_plan op:create_plan
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
function parseToolsYaml(raw) {
|
|
195
|
+
const tools = [];
|
|
196
|
+
const lines = raw.split('\n');
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
if (!trimmed || trimmed === 'tools:')
|
|
200
|
+
continue;
|
|
201
|
+
if (trimmed.startsWith('- ')) {
|
|
202
|
+
tools.push(trimmed.slice(2).trim());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return tools;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=workflow-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-loader.js","sourceRoot":"","sources":["../../src/workflows/workflow-loader.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -36,6 +36,8 @@ import type {
|
|
|
36
36
|
const USAGE_MAX = 10;
|
|
37
37
|
const SPREAD_MAX = 5;
|
|
38
38
|
const RECENCY_DECAY_DAYS = 30;
|
|
39
|
+
const STRENGTH_HALFLIFE_DAYS = 90;
|
|
40
|
+
const STRENGTH_DECAY_FLOOR = 0.3;
|
|
39
41
|
const EXTRACTION_TOOL_THRESHOLD = 3;
|
|
40
42
|
const EXTRACTION_FILE_THRESHOLD = 3;
|
|
41
43
|
const EXTRACTION_HIGH_FEEDBACK_RATIO = 0.8;
|
|
@@ -554,11 +556,21 @@ export class BrainIntelligence {
|
|
|
554
556
|
const successScore = 25 * successRate;
|
|
555
557
|
|
|
556
558
|
// Recency score: max(0, 25 * (1 - daysSince / RECENCY_DECAY_DAYS))
|
|
557
|
-
|
|
559
|
+
// last_used is MAX(created_at) which is unixepoch() (seconds) — convert to ms
|
|
560
|
+
const lastUsedRaw = Number(row.last_used);
|
|
561
|
+
const lastUsedMs = lastUsedRaw < 1e12 ? lastUsedRaw * 1000 : lastUsedRaw;
|
|
558
562
|
const daysSince = (now - lastUsedMs) / (1000 * 60 * 60 * 24);
|
|
559
563
|
const recencyScore = Math.max(0, 25 * (1 - daysSince / RECENCY_DECAY_DAYS));
|
|
560
564
|
|
|
561
|
-
const
|
|
565
|
+
const rawStrength = usageScore + spreadScore + successScore + recencyScore;
|
|
566
|
+
|
|
567
|
+
// Temporal decay multiplier: exponential halflife with floor
|
|
568
|
+
// Patterns fade over time but never vanish completely
|
|
569
|
+
const temporalMultiplier = Math.max(
|
|
570
|
+
STRENGTH_DECAY_FLOOR,
|
|
571
|
+
Math.exp((-Math.LN2 * daysSince) / STRENGTH_HALFLIFE_DAYS),
|
|
572
|
+
);
|
|
573
|
+
const strength = rawStrength * temporalMultiplier;
|
|
562
574
|
|
|
563
575
|
const ps: PatternStrength = {
|
|
564
576
|
pattern,
|
|
@@ -568,6 +580,7 @@ export class BrainIntelligence {
|
|
|
568
580
|
spreadScore,
|
|
569
581
|
successScore,
|
|
570
582
|
recencyScore,
|
|
583
|
+
temporalMultiplier,
|
|
571
584
|
usageCount: row.total,
|
|
572
585
|
uniqueContexts,
|
|
573
586
|
successRate,
|
package/src/brain/types.ts
CHANGED
|
@@ -353,7 +353,8 @@ describe('detectHost', () => {
|
|
|
353
353
|
|
|
354
354
|
it('detects opencode via filesystem config when env vars absent', () => {
|
|
355
355
|
mockedExistsSync.mockImplementation((p: unknown) => {
|
|
356
|
-
|
|
356
|
+
// Normalize to forward slashes so the check works on Windows too
|
|
357
|
+
const path = String(p).replace(/\\/g, '/');
|
|
357
358
|
if (path.includes('opencode/opencode.json')) return true;
|
|
358
359
|
if (path.includes('.claude')) return false;
|
|
359
360
|
return false;
|
|
@@ -364,7 +365,8 @@ describe('detectHost', () => {
|
|
|
364
365
|
|
|
365
366
|
it('detects claude-code via filesystem when .claude dir exists', () => {
|
|
366
367
|
mockedExistsSync.mockImplementation((p: unknown) => {
|
|
367
|
-
|
|
368
|
+
// Normalize to forward slashes so the check works on Windows too
|
|
369
|
+
const path = String(p).replace(/\\/g, '/');
|
|
368
370
|
if (path.includes('.claude')) return true;
|
|
369
371
|
return false;
|
|
370
372
|
});
|
package/src/index.ts
CHANGED
|
@@ -40,9 +40,18 @@ export {
|
|
|
40
40
|
agentKeysPath,
|
|
41
41
|
agentTemplatesDir,
|
|
42
42
|
agentFlagsPath,
|
|
43
|
+
agentKnowledgeDir,
|
|
44
|
+
projectKnowledgeDir,
|
|
43
45
|
sharedVaultPath,
|
|
44
46
|
} from './paths.js';
|
|
45
47
|
|
|
48
|
+
// ─── Vault Markdown Sync ───────────────────────────────────────────
|
|
49
|
+
export {
|
|
50
|
+
syncAllToMarkdown,
|
|
51
|
+
syncEntryToMarkdown,
|
|
52
|
+
entryToMarkdown,
|
|
53
|
+
} from './vault/vault-markdown-sync.js';
|
|
54
|
+
|
|
46
55
|
// ─── Intelligence ────────────────────────────────────────────────────
|
|
47
56
|
export type {
|
|
48
57
|
IntelligenceEntry,
|
|
@@ -901,6 +910,16 @@ export type {
|
|
|
901
910
|
DeclinedCategory,
|
|
902
911
|
} from './operator/operator-context-types.js';
|
|
903
912
|
|
|
913
|
+
// ─── Workflows ─────────────────────────────────────────────────────────
|
|
914
|
+
export {
|
|
915
|
+
WorkflowGateSchema,
|
|
916
|
+
WorkflowOverrideSchema,
|
|
917
|
+
WORKFLOW_TO_INTENT,
|
|
918
|
+
loadAgentWorkflows,
|
|
919
|
+
getWorkflowForIntent,
|
|
920
|
+
} from './workflows/index.js';
|
|
921
|
+
export type { WorkflowGate, WorkflowOverride } from './workflows/index.js';
|
|
922
|
+
|
|
904
923
|
// ─── Update Check ────────────────────────────────────────────────────
|
|
905
924
|
export { checkForUpdate, buildChangelogUrl, detectBreakingChanges } from './update-check.js';
|
|
906
925
|
export type { UpdateInfo } from './update-check.js';
|
package/src/paths.ts
CHANGED
|
@@ -109,6 +109,11 @@ export function agentKnowledgeDir(agentId: string): string {
|
|
|
109
109
|
return join(agentHome(agentId), 'knowledge');
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Project-local knowledge directory for browsable markdown sync. */
|
|
113
|
+
export function projectKnowledgeDir(projectPath: string): string {
|
|
114
|
+
return join(projectPath, 'knowledge');
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
/** Shared vault path: ~/.soleri/vault.db (cross-agent intelligence) */
|
|
113
118
|
export function sharedVaultPath(): string {
|
|
114
119
|
return join(SOLERI_HOME, 'vault.db');
|
|
@@ -172,12 +172,16 @@ export function analyzeStructure(plan: Plan): PlanGap[] {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
if (plan.tasks.length === 0) {
|
|
175
|
+
const hasApproachSteps =
|
|
176
|
+
plan.approach && /(?:step\s+\d|task\s+\d|\d\.\s|\d\)\s)/i.test(plan.approach);
|
|
175
177
|
gaps.push(
|
|
176
178
|
gap(
|
|
177
|
-
'critical',
|
|
179
|
+
hasApproachSteps ? 'major' : 'critical',
|
|
178
180
|
'structure',
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
hasApproachSteps
|
|
182
|
+
? 'Plan has no tasks but approach contains steps. Use `addTasks` in `plan_iterate` or pass `tasks` in `create_plan` to promote them.'
|
|
183
|
+
: 'Plan has no tasks.',
|
|
184
|
+
'Add tasks via `create_plan` (tasks param) or `plan_iterate` (addTasks param).',
|
|
181
185
|
'tasks',
|
|
182
186
|
'no_tasks',
|
|
183
187
|
),
|
|
@@ -14,12 +14,14 @@ vi.mock('../vault/scope-detector.js', () => ({
|
|
|
14
14
|
})),
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
+
const mockSyncEntryToMarkdown = vi.fn(() => Promise.resolve({ written: true }));
|
|
17
18
|
vi.mock('../vault/vault-markdown-sync.js', () => ({
|
|
18
|
-
syncEntryToMarkdown:
|
|
19
|
+
syncEntryToMarkdown: (...args: unknown[]) => mockSyncEntryToMarkdown(...args),
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
22
|
vi.mock('../paths.js', () => ({
|
|
22
23
|
agentKnowledgeDir: vi.fn(() => '/mock/knowledge'),
|
|
24
|
+
projectKnowledgeDir: vi.fn((p: string) => `/mock/project/${p}/knowledge`),
|
|
23
25
|
}));
|
|
24
26
|
|
|
25
27
|
// ─── Mock Runtime Factory ──────────────────────────────────────────────
|
|
@@ -213,6 +215,32 @@ describe('createCaptureOps', () => {
|
|
|
213
215
|
const results = result.results as Array<Record<string, unknown>>;
|
|
214
216
|
expect((results[0].scope as Record<string, unknown>).tier).toBe('agent');
|
|
215
217
|
});
|
|
218
|
+
|
|
219
|
+
it('triggers markdown file write on successful capture', async () => {
|
|
220
|
+
mockSyncEntryToMarkdown.mockClear();
|
|
221
|
+
await findOp(ops, 'capture_knowledge').handler({
|
|
222
|
+
entries: [
|
|
223
|
+
{ type: 'pattern', domain: 'testing', title: 'Sync Test', description: 'test', tags: [] },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
// fire-and-forget: allow microtask to flush
|
|
227
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
228
|
+
expect(mockSyncEntryToMarkdown).toHaveBeenCalledWith(
|
|
229
|
+
expect.objectContaining({ domain: 'testing', title: 'Sync Test' }),
|
|
230
|
+
'/mock/knowledge',
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not block capture response when sync fails', async () => {
|
|
235
|
+
mockSyncEntryToMarkdown.mockClear();
|
|
236
|
+
mockSyncEntryToMarkdown.mockRejectedValueOnce(new Error('disk full'));
|
|
237
|
+
const result = (await findOp(ops, 'capture_knowledge').handler({
|
|
238
|
+
entries: [{ type: 'pattern', domain: 'a', title: 'A', description: 'a', tags: [] }],
|
|
239
|
+
})) as Record<string, unknown>;
|
|
240
|
+
// Capture should still succeed despite sync error
|
|
241
|
+
expect(result.captured).toBe(1);
|
|
242
|
+
expect(result.rejected).toBe(0);
|
|
243
|
+
});
|
|
216
244
|
});
|
|
217
245
|
|
|
218
246
|
describe('capture_quick', () => {
|
|
@@ -323,6 +351,35 @@ describe('createCaptureOps', () => {
|
|
|
323
351
|
expect(result.captured).toBe(false);
|
|
324
352
|
expect((result.governance as Record<string, unknown>).action).toBe('error');
|
|
325
353
|
});
|
|
354
|
+
|
|
355
|
+
it('triggers markdown file write on successful capture', async () => {
|
|
356
|
+
mockSyncEntryToMarkdown.mockClear();
|
|
357
|
+
await findOp(ops, 'capture_quick').handler({
|
|
358
|
+
type: 'pattern',
|
|
359
|
+
domain: 'testing',
|
|
360
|
+
title: 'Quick Sync Test',
|
|
361
|
+
description: 'quick test',
|
|
362
|
+
});
|
|
363
|
+
// fire-and-forget: allow microtask to flush
|
|
364
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
365
|
+
expect(mockSyncEntryToMarkdown).toHaveBeenCalledWith(
|
|
366
|
+
expect.objectContaining({ domain: 'testing', title: 'Quick Sync Test' }),
|
|
367
|
+
'/mock/knowledge',
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('does not block capture response when sync fails', async () => {
|
|
372
|
+
mockSyncEntryToMarkdown.mockClear();
|
|
373
|
+
mockSyncEntryToMarkdown.mockRejectedValueOnce(new Error('disk full'));
|
|
374
|
+
const result = (await findOp(ops, 'capture_quick').handler({
|
|
375
|
+
type: 'pattern',
|
|
376
|
+
domain: 'testing',
|
|
377
|
+
title: 'Quick Fail',
|
|
378
|
+
description: 'test',
|
|
379
|
+
})) as Record<string, unknown>;
|
|
380
|
+
// Capture should still succeed despite sync error
|
|
381
|
+
expect(result.captured).toBe(true);
|
|
382
|
+
});
|
|
326
383
|
});
|
|
327
384
|
|
|
328
385
|
describe('search_intelligent', () => {
|
|
@@ -11,7 +11,7 @@ import type { AgentRuntime } from './types.js';
|
|
|
11
11
|
import { detectScope } from '../vault/scope-detector.js';
|
|
12
12
|
import type { ScopeTier, ScopeDetectionResult } from '../vault/scope-detector.js';
|
|
13
13
|
import { syncEntryToMarkdown } from '../vault/vault-markdown-sync.js';
|
|
14
|
-
import { agentKnowledgeDir } from '../paths.js';
|
|
14
|
+
import { agentKnowledgeDir, projectKnowledgeDir } from '../paths.js';
|
|
15
15
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -189,6 +189,7 @@ export function createCaptureOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
189
189
|
origin: 'user',
|
|
190
190
|
},
|
|
191
191
|
config.agentId,
|
|
192
|
+
projectPath,
|
|
192
193
|
);
|
|
193
194
|
}
|
|
194
195
|
} catch (err) {
|
|
@@ -417,6 +418,7 @@ export function createCaptureOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
417
418
|
origin: 'user',
|
|
418
419
|
},
|
|
419
420
|
config.agentId,
|
|
421
|
+
projectPath,
|
|
420
422
|
);
|
|
421
423
|
return result;
|
|
422
424
|
} catch (err) {
|
|
@@ -629,9 +631,18 @@ function mapType(type: string): 'pattern' | 'anti-pattern' | 'rule' | 'playbook'
|
|
|
629
631
|
}
|
|
630
632
|
|
|
631
633
|
/** Fire-and-forget markdown sync — never blocks capture, logs errors silently. */
|
|
632
|
-
function fireAndForgetSync(entry: IntelligenceEntry, agentId: string): void {
|
|
633
|
-
|
|
634
|
-
|
|
634
|
+
function fireAndForgetSync(entry: IntelligenceEntry, agentId: string, projectPath?: string): void {
|
|
635
|
+
// Always sync to agent home dir
|
|
636
|
+
const agentDir = agentKnowledgeDir(agentId);
|
|
637
|
+
syncEntryToMarkdown(entry, agentDir).catch(() => {
|
|
635
638
|
/* non-blocking — markdown sync is best-effort */
|
|
636
639
|
});
|
|
640
|
+
|
|
641
|
+
// Also sync to project-local knowledge dir if a real project path is provided
|
|
642
|
+
if (projectPath && projectPath !== '.') {
|
|
643
|
+
const projDir = projectKnowledgeDir(projectPath);
|
|
644
|
+
syncEntryToMarkdown(entry, projDir).catch(() => {
|
|
645
|
+
/* non-blocking — markdown sync is best-effort */
|
|
646
|
+
});
|
|
647
|
+
}
|
|
637
648
|
}
|