vgxness 1.20.13 → 1.20.15
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/README.md +2 -0
- package/dist/adapters/opencode/install/client-install-opencode-contract.js +4 -2
- package/dist/adapters/opencode/install/client-install-opencode.js +6 -2
- package/dist/application/agents/agent-resolver.js +9 -1
- package/dist/application/knowledge-workspace/canvas-renderer.js +36 -0
- package/dist/application/knowledge-workspace/filesystem-workspace-port.js +98 -0
- package/dist/application/knowledge-workspace/markdown-renderer.js +68 -0
- package/dist/application/knowledge-workspace/obsidian-export-service.js +157 -0
- package/dist/application/knowledge-workspace/obsidian-mcp-workspace-port.js +109 -0
- package/dist/application/knowledge-workspace/workspace-port.js +20 -0
- package/dist/application/provider-setup/provider-doctor.js +1 -1
- package/dist/domain/agents/canonical-agent-manifest.js +0 -1
- package/dist/domain/knowledge-workspace/schema.js +1 -0
- package/dist/interfaces/cli/cli-help.js +5 -1
- package/dist/interfaces/cli/commands/index.js +1 -0
- package/dist/interfaces/cli/commands/knowledge-workspace-dispatcher.js +252 -0
- package/dist/interfaces/cli/dispatcher.js +9 -1
- package/docs/architecture.md +1 -1
- package/docs/cli.md +13 -0
- package/docs/glossary.md +1 -1
- package/docs/knowledge-workspace.md +135 -0
- package/docs/obsidian-mcp.md +130 -0
- package/docs/sdd/sqlite-obsidian-knowledge-workflow/design.md +118 -0
- package/docs/sdd/sqlite-obsidian-knowledge-workflow/proposal.md +80 -0
- package/docs/sdd/sqlite-obsidian-knowledge-workflow/spec.md +88 -0
- package/docs/sdd/sqlite-obsidian-knowledge-workflow/tasks.md +70 -0
- package/docs/storage.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -269,6 +269,8 @@ Remove any user-global OpenCode/Claude config entries and local/global VGXNESS d
|
|
|
269
269
|
- [Project health audit v1.10.x](./docs/project-health-audit-v1.10.x.md)
|
|
270
270
|
- [Project health audit v1.9.1](./docs/project-health-audit-v1.9.1.md)
|
|
271
271
|
- [Architecture](./docs/architecture.md)
|
|
272
|
+
- [Knowledge Workspace](./docs/knowledge-workspace.md)
|
|
273
|
+
- [Obsidian MCP integration](./docs/obsidian-mcp.md)
|
|
272
274
|
- [MCP tools](./docs/mcp.md)
|
|
273
275
|
- [Safety model](./docs/safety.md)
|
|
274
276
|
- [Storage](./docs/storage.md)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { canonicalCapabilitySubagentLegacyAliases } from '../../../agents/canonical-agent-manifest.js';
|
|
3
4
|
import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, } from './opencode-default-agent-config.js';
|
|
4
5
|
const opencodeConfigSchema = 'https://opencode.ai/config.json';
|
|
5
6
|
const opencodeUserGlobalOnlyMessage = 'VGX-managed OpenCode configuration is user-global only. Project/local OpenCode files are treated as external/manual diagnostics and will not be written by VGXNESS. Re-run without the project/local install scope.';
|
|
@@ -141,7 +142,8 @@ export function findConflictingVgxnessAgents(config, agentPlan, input = {}) {
|
|
|
141
142
|
if (!agentPlan.installsAgents || !isRecord(config.agent))
|
|
142
143
|
return [];
|
|
143
144
|
const defaults = createOpenCodeDefaultAgentConfig(input).agents;
|
|
144
|
-
|
|
145
|
+
const managedNames = [...agentPlan.agentNames, ...Object.keys(canonicalCapabilitySubagentLegacyAliases)];
|
|
146
|
+
return managedNames.filter((name) => Object.hasOwn(config.agent, name) && !deepEqual(config.agent[name], defaults[name]));
|
|
145
147
|
}
|
|
146
148
|
function deepEqual(left, right) {
|
|
147
149
|
if (Object.is(left, right))
|
|
@@ -199,7 +201,7 @@ function warningsForScope(_scope, overwriteVgxness, agentPlan, bashPermissionPol
|
|
|
199
201
|
: [];
|
|
200
202
|
const bashWarnings = bashPermissionPolicy.manager === 'deny'
|
|
201
203
|
? [
|
|
202
|
-
'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; capability subagents keep explicit permissions
|
|
204
|
+
'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; capability subagents keep explicit canonical permissions only. This is config-level evidence only and does not verify provider runtime enforcement.',
|
|
203
205
|
]
|
|
204
206
|
: ['OpenCode top-level permission.bash is set to ask.'];
|
|
205
207
|
return [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { canonicalCapabilitySubagentLegacyAliases } from '../../../agents/canonical-agent-manifest.js';
|
|
3
4
|
import { createManagedProviderConfigBackup } from '../../../setup/backup-rollback-service.js';
|
|
4
5
|
import { findConflictingVgxnessAgents, planOpenCodeMcpInstall, } from './client-install-opencode-contract.js';
|
|
5
6
|
import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, vgxnessOpenCodeInstructionsPath, } from './opencode-default-agent-config.js';
|
|
@@ -73,7 +74,10 @@ function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveMan
|
|
|
73
74
|
const defaults = createOpenCodeDefaultAgentConfig({ effectiveManagerInstructions });
|
|
74
75
|
merged.instructions = mergeOpenCodeInstructions(config.instructions);
|
|
75
76
|
merged.default_agent = defaults.defaultAgent;
|
|
76
|
-
|
|
77
|
+
const existingAgents = isRecord(config.agent) ? { ...config.agent } : {};
|
|
78
|
+
for (const legacyName of Object.keys(canonicalCapabilitySubagentLegacyAliases))
|
|
79
|
+
delete existingAgents[legacyName];
|
|
80
|
+
merged.agent = { ...existingAgents, ...defaults.agents };
|
|
77
81
|
}
|
|
78
82
|
merged.permission = { ...(isRecord(config.permission) ? config.permission : {}), bash: 'ask' };
|
|
79
83
|
return merged;
|
|
@@ -216,7 +220,7 @@ function confirmationRequiredMessage(scope) {
|
|
|
216
220
|
function warnings() {
|
|
217
221
|
return [
|
|
218
222
|
'Restart OpenCode after installation so it reloads the project MCP config.',
|
|
219
|
-
'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; capability subagents keep explicit permissions
|
|
223
|
+
'OpenCode config sets top-level permission.bash = ask; manager read/search discovery tools (read/glob/grep) are allowed while manager bash/edit/write are denied; capability subagents keep explicit canonical permissions only. This is config-level evidence only and does not verify provider runtime enforcement.',
|
|
220
224
|
];
|
|
221
225
|
}
|
|
222
226
|
function manualTest(databasePath, source) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalCapabilitySubagentLegacyAliases } from '../../domain/agents/canonical-agent-manifest.js';
|
|
1
2
|
import { normalizeSddPhaseInput } from '../../sdd/schema.js';
|
|
2
3
|
import { agentLookupContexts } from './agent-lookup-contexts.js';
|
|
3
4
|
export class AgentResolver {
|
|
@@ -12,7 +13,7 @@ export class AgentResolver {
|
|
|
12
13
|
const normalized = normalizeInput(input);
|
|
13
14
|
const candidates = [];
|
|
14
15
|
const skipped = [];
|
|
15
|
-
for (const agent of agents.value) {
|
|
16
|
+
for (const agent of suppressLegacyAliasAgents(agents.value)) {
|
|
16
17
|
const skipReasons = hardSkipReasons(agent, normalized);
|
|
17
18
|
if (skipReasons.length > 0) {
|
|
18
19
|
skipped.push(toSkipped(agent, skipReasons));
|
|
@@ -44,6 +45,13 @@ export class AgentResolver {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
function suppressLegacyAliasAgents(agents) {
|
|
49
|
+
const names = new Set(agents.map((agent) => agent.name));
|
|
50
|
+
return agents.filter((agent) => {
|
|
51
|
+
const canonical = canonicalCapabilitySubagentLegacyAliases[agent.name];
|
|
52
|
+
return canonical === undefined || !names.has(canonical);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
47
55
|
const SDD_PHASE_SEMANTICS = [
|
|
48
56
|
{
|
|
49
57
|
name: 'sdd-planning',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function renderObsidianCanvas(input) {
|
|
2
|
+
const nodes = [...input.nodes].sort(compareById).map(mapNode);
|
|
3
|
+
const edges = [...input.edges].sort(compareById).map(mapEdge);
|
|
4
|
+
return `${JSON.stringify({ nodes, edges }, null, 2)}\n`;
|
|
5
|
+
}
|
|
6
|
+
function mapNode(node) {
|
|
7
|
+
const base = {
|
|
8
|
+
id: node.id,
|
|
9
|
+
x: node.x,
|
|
10
|
+
y: node.y,
|
|
11
|
+
width: node.width ?? 360,
|
|
12
|
+
height: node.height ?? 220,
|
|
13
|
+
};
|
|
14
|
+
if (node.type === 'file') {
|
|
15
|
+
if (node.file === undefined || node.file.trim() === '') {
|
|
16
|
+
throw new Error(`Canvas file node requires a file path: ${node.id}`);
|
|
17
|
+
}
|
|
18
|
+
return { ...base, type: 'file', file: node.file };
|
|
19
|
+
}
|
|
20
|
+
return { ...base, type: 'text', text: node.label };
|
|
21
|
+
}
|
|
22
|
+
function mapEdge(edge) {
|
|
23
|
+
const mapped = {
|
|
24
|
+
id: edge.id,
|
|
25
|
+
fromNode: edge.fromNode,
|
|
26
|
+
fromSide: edge.fromSide ?? 'right',
|
|
27
|
+
toNode: edge.toNode,
|
|
28
|
+
toSide: edge.toSide ?? 'left',
|
|
29
|
+
};
|
|
30
|
+
if (edge.label !== undefined)
|
|
31
|
+
mapped.label = edge.label;
|
|
32
|
+
return mapped;
|
|
33
|
+
}
|
|
34
|
+
function compareById(a, b) {
|
|
35
|
+
return a.id.localeCompare(b.id);
|
|
36
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { assertSafeWorkspaceRelativePath, isGeneratedByVgxness, normalizeWorkspacePath, } from './workspace-port.js';
|
|
4
|
+
export class FilesystemWorkspacePort {
|
|
5
|
+
vaultRoot;
|
|
6
|
+
workspaceRoot;
|
|
7
|
+
absoluteWorkspaceRoot;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.vaultRoot = resolve(options.vaultRoot);
|
|
10
|
+
this.workspaceRoot = normalizeWorkspacePath(options.workspaceRoot ?? 'VGXNESS');
|
|
11
|
+
this.absoluteWorkspaceRoot = resolve(this.vaultRoot, this.workspaceRoot);
|
|
12
|
+
}
|
|
13
|
+
async status() {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'knowledge-workspace-status',
|
|
16
|
+
status: existsSync(this.absoluteWorkspaceRoot) ? 'available' : 'unavailable',
|
|
17
|
+
mode: 'filesystem',
|
|
18
|
+
workspaceRoot: this.absoluteWorkspaceRoot,
|
|
19
|
+
diagnostics: existsSync(this.absoluteWorkspaceRoot) ? [] : ['Workspace root does not exist yet.'],
|
|
20
|
+
safety: { readOnly: true, writesNotes: false, deletesNotes: false, executesObsidianCommands: false },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async listNotes(input) {
|
|
24
|
+
const absolute = this.resolveContained(input.pathPrefix, false);
|
|
25
|
+
if (!existsSync(absolute))
|
|
26
|
+
return [];
|
|
27
|
+
const stat = statSync(absolute);
|
|
28
|
+
if (stat.isFile())
|
|
29
|
+
return [{ path: this.toWorkspacePath(absolute), type: 'file' }];
|
|
30
|
+
const entries = readdirSync(absolute, { withFileTypes: true });
|
|
31
|
+
const refs = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const child = join(absolute, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
refs.push({ path: this.toWorkspacePath(child), type: 'directory' });
|
|
36
|
+
if (input.recursive ?? true)
|
|
37
|
+
refs.push(...(await this.listNotes({ pathPrefix: this.toWorkspacePath(child), recursive: true })));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
refs.push({ path: this.toWorkspacePath(child), type: 'file' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return refs.sort((a, b) => a.path.localeCompare(b.path));
|
|
44
|
+
}
|
|
45
|
+
async getNote(input) {
|
|
46
|
+
const absolute = this.resolveContained(input.path, false);
|
|
47
|
+
return { path: this.toWorkspacePath(absolute), content: readFileSync(absolute, 'utf8') };
|
|
48
|
+
}
|
|
49
|
+
async writeGeneratedNote(input) {
|
|
50
|
+
const absolute = this.resolveContained(input.path, true);
|
|
51
|
+
const exists = existsSync(absolute);
|
|
52
|
+
const previousContent = exists ? readFileSync(absolute, 'utf8') : undefined;
|
|
53
|
+
if (previousContent !== undefined && input.overwriteGeneratedOnly && !isGeneratedByVgxness(previousContent)) {
|
|
54
|
+
throw new Error(`Refusing to overwrite non-generated Obsidian note: ${this.toWorkspacePath(absolute)}`);
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
57
|
+
writeFileSync(absolute, input.content, 'utf8');
|
|
58
|
+
const output = {
|
|
59
|
+
path: this.toWorkspacePath(absolute),
|
|
60
|
+
created: !exists,
|
|
61
|
+
currentSizeInBytes: Buffer.byteLength(input.content),
|
|
62
|
+
};
|
|
63
|
+
if (previousContent !== undefined)
|
|
64
|
+
output.previousSizeInBytes = Buffer.byteLength(previousContent);
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
async patchNote(input) {
|
|
68
|
+
const note = await this.getNote({ path: input.path });
|
|
69
|
+
const marker = `## ${input.section}`;
|
|
70
|
+
if (!note.content.includes(marker))
|
|
71
|
+
throw new Error(`Section not found: ${input.section}`);
|
|
72
|
+
const nextContent = applySimpleHeadingPatch(note.content, marker, input.operation, input.content);
|
|
73
|
+
return this.writeGeneratedNote({ path: note.path, content: nextContent, overwriteGeneratedOnly: false });
|
|
74
|
+
}
|
|
75
|
+
resolveContained(path, allowNonexistentLeaf) {
|
|
76
|
+
const relativePath = assertSafeWorkspaceRelativePath(path, this.workspaceRoot);
|
|
77
|
+
const absolute = resolve(this.vaultRoot, relativePath);
|
|
78
|
+
if (!isPathInside(absolute, this.absoluteWorkspaceRoot))
|
|
79
|
+
throw new Error(`Path must stay under ${this.absoluteWorkspaceRoot}: ${path}`);
|
|
80
|
+
if (!allowNonexistentLeaf && !existsSync(absolute))
|
|
81
|
+
throw new Error(`Note not found: ${relativePath}`);
|
|
82
|
+
return absolute;
|
|
83
|
+
}
|
|
84
|
+
toWorkspacePath(absolutePath) {
|
|
85
|
+
const relativePath = relative(this.vaultRoot, absolutePath).replaceAll(sep, '/');
|
|
86
|
+
return normalizeWorkspacePath(relativePath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function applySimpleHeadingPatch(content, marker, operation, patch) {
|
|
90
|
+
if (operation === 'prepend')
|
|
91
|
+
return content.replace(marker, `${patch}\n${marker}`);
|
|
92
|
+
if (operation === 'append')
|
|
93
|
+
return content.replace(marker, `${marker}\n${patch}`);
|
|
94
|
+
return content.replace(marker, patch);
|
|
95
|
+
}
|
|
96
|
+
function isPathInside(path, root) {
|
|
97
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
98
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function renderGeneratedFrontmatter(metadata) {
|
|
2
|
+
return [
|
|
3
|
+
'---',
|
|
4
|
+
'generated_by: vgxness',
|
|
5
|
+
`source_of_truth: ${metadata.sourceOfTruth}`,
|
|
6
|
+
'manual_edits: overwritten_on_export',
|
|
7
|
+
`transport: ${metadata.transport}`,
|
|
8
|
+
`generated_at: ${quoteYamlScalar(metadata.generatedAt)}`,
|
|
9
|
+
`project: ${quoteYamlScalar(metadata.project)}`,
|
|
10
|
+
'---',
|
|
11
|
+
'',
|
|
12
|
+
].join('\n');
|
|
13
|
+
}
|
|
14
|
+
export function renderMemorySummaryMarkdown(input) {
|
|
15
|
+
const observations = [...input.observations].sort(compareMemoryItems);
|
|
16
|
+
const frontmatter = renderGeneratedFrontmatter({
|
|
17
|
+
generatedBy: 'vgxness',
|
|
18
|
+
sourceOfTruth: 'sqlite',
|
|
19
|
+
manualEdits: 'overwritten_on_export',
|
|
20
|
+
transport: input.transport ?? 'preview',
|
|
21
|
+
generatedAt: input.generatedAt,
|
|
22
|
+
project: input.project,
|
|
23
|
+
});
|
|
24
|
+
const rows = observations.map((item) => [item.scope, item.type, item.title, item.topicKey ?? '', item.updatedAt, item.preview].map(escapeTableCell).join(' | '));
|
|
25
|
+
return `${frontmatter}# ${input.project} Memory Summary\n\n> Generated by VGXNESS from SQLite memory previews. SQLite remains the source of truth; this note is a human-facing projection.\n\n${rows.length === 0
|
|
26
|
+
? 'No memory observations matched this export.\n'
|
|
27
|
+
: ['| Scope | Type | Title | Topic Key | Updated | Preview |', '|---|---|---|---|---|---|', ...rows.map((row) => `| ${row} |`)].join('\n') + '\n'}`;
|
|
28
|
+
}
|
|
29
|
+
export function renderSddBoardMarkdown(input) {
|
|
30
|
+
const items = [...input.items].sort(compareSddItems);
|
|
31
|
+
const frontmatter = renderGeneratedFrontmatter({
|
|
32
|
+
generatedBy: 'vgxness',
|
|
33
|
+
sourceOfTruth: 'sqlite',
|
|
34
|
+
manualEdits: 'overwritten_on_export',
|
|
35
|
+
transport: input.transport ?? 'preview',
|
|
36
|
+
generatedAt: input.generatedAt,
|
|
37
|
+
project: input.project,
|
|
38
|
+
});
|
|
39
|
+
const rows = items.map((item) => [item.changeId, item.phase, item.state, item.updatedAt ?? '', item.notePath === undefined ? '' : `[[${item.notePath}]]`].map(escapeTableCell).join(' | '));
|
|
40
|
+
const grouped = groupSddItemsByState(items);
|
|
41
|
+
const summary = [...grouped.entries()]
|
|
42
|
+
.map(([state, stateItems]) => `- ${state}: ${stateItems.length}`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
return `${frontmatter}# ${input.project} SDD Board\n\n> Generated by VGXNESS from SQLite SDD artifact/governance state. Obsidian does not prove acceptance, readiness, verification, or authorization.\n\n## Summary\n\n${summary || '- No SDD items matched this export.'}\n\n## Board\n\n${rows.length === 0
|
|
45
|
+
? 'No SDD items matched this export.\n'
|
|
46
|
+
: ['| Change | Phase | State | Updated | Note |', '|---|---|---|---|---|', ...rows.map((row) => `| ${row} |`)].join('\n') + '\n'}`;
|
|
47
|
+
}
|
|
48
|
+
function compareMemoryItems(a, b) {
|
|
49
|
+
return b.updatedAt.localeCompare(a.updatedAt) || a.scope.localeCompare(b.scope) || a.type.localeCompare(b.type) || a.title.localeCompare(b.title);
|
|
50
|
+
}
|
|
51
|
+
function compareSddItems(a, b) {
|
|
52
|
+
return a.changeId.localeCompare(b.changeId) || a.phase.localeCompare(b.phase) || a.state.localeCompare(b.state);
|
|
53
|
+
}
|
|
54
|
+
function groupSddItemsByState(items) {
|
|
55
|
+
const groups = new Map();
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const existing = groups.get(item.state) ?? [];
|
|
58
|
+
existing.push(item);
|
|
59
|
+
groups.set(item.state, existing);
|
|
60
|
+
}
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
function escapeTableCell(value) {
|
|
64
|
+
return value.replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|').trim();
|
|
65
|
+
}
|
|
66
|
+
function quoteYamlScalar(value) {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { normalizeSddArtifact } from '../../domain/sdd/schema.js';
|
|
2
|
+
import { renderGeneratedFrontmatter, renderMemorySummaryMarkdown, renderSddBoardMarkdown } from './markdown-renderer.js';
|
|
3
|
+
export class KnowledgeWorkspaceExportService {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
async export(input) {
|
|
9
|
+
const status = await this.options.workspace.status();
|
|
10
|
+
if (status.status !== 'available') {
|
|
11
|
+
return { ok: false, error: { code: 'validation_failed', message: `Knowledge workspace unavailable: ${status.diagnostics.join('; ') || status.mode}` } };
|
|
12
|
+
}
|
|
13
|
+
const transport = status.mode;
|
|
14
|
+
const kinds = input.kind === 'all' ? ['docs', 'memory', 'sdd', 'canvas'] : [input.kind];
|
|
15
|
+
const exportedNotes = [];
|
|
16
|
+
for (const kind of kinds) {
|
|
17
|
+
if (kind === 'docs')
|
|
18
|
+
exportedNotes.push(await this.exportDocs(input.project, transport));
|
|
19
|
+
if (kind === 'memory') {
|
|
20
|
+
const memory = this.requireReadModel();
|
|
21
|
+
if (!memory.ok)
|
|
22
|
+
return memory;
|
|
23
|
+
const exported = await this.exportMemorySummary({ project: input.project, transport, readModel: memory.value, ...(input.limit === undefined ? {} : { limit: input.limit }) });
|
|
24
|
+
if (!exported.ok)
|
|
25
|
+
return exported;
|
|
26
|
+
exportedNotes.push(exported.value);
|
|
27
|
+
}
|
|
28
|
+
if (kind === 'sdd') {
|
|
29
|
+
const memory = this.requireReadModel();
|
|
30
|
+
if (!memory.ok)
|
|
31
|
+
return memory;
|
|
32
|
+
const exported = await this.exportSdd({ project: input.project, transport, readModel: memory.value });
|
|
33
|
+
if (!exported.ok)
|
|
34
|
+
return exported;
|
|
35
|
+
exportedNotes.push(...exported.value);
|
|
36
|
+
}
|
|
37
|
+
if (kind === 'canvas')
|
|
38
|
+
exportedNotes.push(await this.exportArchitectureCanvas(input.project));
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
value: {
|
|
43
|
+
kind: 'knowledge-workspace-export',
|
|
44
|
+
project: input.project,
|
|
45
|
+
requestedKind: input.kind,
|
|
46
|
+
transport,
|
|
47
|
+
exportedNotes,
|
|
48
|
+
safety: {
|
|
49
|
+
sqliteRemainsSourceOfTruth: true,
|
|
50
|
+
overwritesGeneratedNotesOnly: true,
|
|
51
|
+
exportsMemoryPreviewsOnly: true,
|
|
52
|
+
infersSddAcceptanceFromObsidian: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async exportDocs(project, transport) {
|
|
58
|
+
const content = `${renderGeneratedFrontmatter({
|
|
59
|
+
generatedBy: 'vgxness',
|
|
60
|
+
sourceOfTruth: 'repository-docs',
|
|
61
|
+
manualEdits: 'overwritten_on_export',
|
|
62
|
+
transport,
|
|
63
|
+
generatedAt: this.now(),
|
|
64
|
+
project,
|
|
65
|
+
})}# ${project} Knowledge Workspace\n\nThis workspace is a human-facing projection generated by VGXNESS.\n\n## Boundaries\n\n- SQLite remains the source of truth for memory, SDD governance, runs, traces, and acceptance.\n- Obsidian is for review, planning, docs, boards, and canvas navigation.\n- Generated notes disclose that manual edits may be overwritten on export.\n`;
|
|
66
|
+
return toExportedNote(await this.options.workspace.writeGeneratedNote({ path: `VGXNESS/00 Home/${project} Knowledge Workspace.md`, content, overwriteGeneratedOnly: true }), 'docs');
|
|
67
|
+
}
|
|
68
|
+
async exportMemorySummary(input) {
|
|
69
|
+
const previews = input.readModel.searchObservationPreviewsNoTrace({ project: input.project, limit: input.limit ?? 50 });
|
|
70
|
+
if (!previews.ok)
|
|
71
|
+
return previews;
|
|
72
|
+
const content = renderMemorySummaryMarkdown({
|
|
73
|
+
project: input.project,
|
|
74
|
+
generatedAt: this.now(),
|
|
75
|
+
transport: input.transport,
|
|
76
|
+
observations: previews.value.map((observation) => ({
|
|
77
|
+
title: observation.title,
|
|
78
|
+
type: observation.type,
|
|
79
|
+
scope: observation.scope,
|
|
80
|
+
preview: observation.preview,
|
|
81
|
+
updatedAt: observation.updatedAt,
|
|
82
|
+
...(observation.topicKey === undefined ? {} : { topicKey: observation.topicKey }),
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
const result = await this.options.workspace.writeGeneratedNote({
|
|
86
|
+
path: `VGXNESS/06 Memory Summaries/${input.project}.md`,
|
|
87
|
+
content,
|
|
88
|
+
overwriteGeneratedOnly: true,
|
|
89
|
+
});
|
|
90
|
+
return { ok: true, value: toExportedNote(result, 'memory') };
|
|
91
|
+
}
|
|
92
|
+
async exportSdd(input) {
|
|
93
|
+
const artifacts = input.readModel.listArtifactsByTopicPrefixNoTrace(input.project, 'sdd/');
|
|
94
|
+
if (!artifacts.ok)
|
|
95
|
+
return artifacts;
|
|
96
|
+
const artifactNotes = [];
|
|
97
|
+
const boardItems = [];
|
|
98
|
+
for (const artifact of artifacts.value) {
|
|
99
|
+
const parsedTopic = parseSddTopicKey(artifact.topicKey);
|
|
100
|
+
if (parsedTopic === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
const normalized = normalizeSddArtifact(artifact);
|
|
103
|
+
const notePath = `VGXNESS/03 SDD/${parsedTopic.change}/${parsedTopic.phase}.md`;
|
|
104
|
+
const content = `${renderGeneratedFrontmatter({
|
|
105
|
+
generatedBy: 'vgxness',
|
|
106
|
+
sourceOfTruth: 'sqlite',
|
|
107
|
+
manualEdits: 'overwritten_on_export',
|
|
108
|
+
transport: input.transport,
|
|
109
|
+
generatedAt: this.now(),
|
|
110
|
+
project: input.project,
|
|
111
|
+
})}# ${parsedTopic.change} / ${parsedTopic.phase}\n\n> Generated from SQLite SDD artifact state. Obsidian does not prove readiness or acceptance.\n\n## State\n\n- Status: ${normalized.metadata.status}\n- Accepted: ${normalized.metadata.status === 'accepted' ? 'yes' : 'no'}\n- Updated: ${artifact.updatedAt}\n\n## Artifact\n\n${artifact.content}\n`;
|
|
112
|
+
const result = await this.options.workspace.writeGeneratedNote({ path: notePath, content, overwriteGeneratedOnly: true });
|
|
113
|
+
artifactNotes.push(toExportedNote(result, 'sdd'));
|
|
114
|
+
boardItems.push({
|
|
115
|
+
changeId: parsedTopic.change,
|
|
116
|
+
phase: parsedTopic.phase,
|
|
117
|
+
state: normalized.metadata.status,
|
|
118
|
+
updatedAt: artifact.updatedAt,
|
|
119
|
+
notePath,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const board = renderSddBoardMarkdown({ project: input.project, generatedAt: this.now(), transport: input.transport, items: boardItems });
|
|
123
|
+
const boardResult = await this.options.workspace.writeGeneratedNote({
|
|
124
|
+
path: 'VGXNESS/08 Boards/SDD Board.md',
|
|
125
|
+
content: board,
|
|
126
|
+
overwriteGeneratedOnly: true,
|
|
127
|
+
});
|
|
128
|
+
return { ok: true, value: [...artifactNotes, toExportedNote(boardResult, 'sdd')] };
|
|
129
|
+
}
|
|
130
|
+
async exportArchitectureCanvas(project) {
|
|
131
|
+
const content = `${JSON.stringify({
|
|
132
|
+
nodes: [
|
|
133
|
+
{ id: 'sqlite', type: 'text', text: 'SQLite\nOperational source of truth', x: 0, y: 0, width: 360, height: 180 },
|
|
134
|
+
{ id: 'obsidian', type: 'text', text: 'Obsidian\nHuman-facing projection workspace', x: 520, y: 0, width: 360, height: 180 },
|
|
135
|
+
],
|
|
136
|
+
edges: [{ id: 'project', fromNode: 'sqlite', fromSide: 'right', toNode: 'obsidian', toSide: 'left', label: 'project via MCP/export' }],
|
|
137
|
+
}, null, 2)}\n`;
|
|
138
|
+
return toExportedNote(await this.options.workspace.writeGeneratedNote({ path: `VGXNESS/07 Canvas/${project} Knowledge Architecture.canvas`, content, overwriteGeneratedOnly: true }), 'canvas');
|
|
139
|
+
}
|
|
140
|
+
requireReadModel() {
|
|
141
|
+
if (this.options.readModel !== undefined)
|
|
142
|
+
return { ok: true, value: this.options.readModel };
|
|
143
|
+
return { ok: false, error: { code: 'validation_failed', message: 'Knowledge export kind requires a SQLite read model' } };
|
|
144
|
+
}
|
|
145
|
+
now() {
|
|
146
|
+
return this.options.now?.() ?? new Date().toISOString();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function toExportedNote(result, kind) {
|
|
150
|
+
return { path: result.path, created: result.created, kind };
|
|
151
|
+
}
|
|
152
|
+
function parseSddTopicKey(topicKey) {
|
|
153
|
+
const match = /^sdd\/([^/]+)\/([^/]+)$/u.exec(topicKey);
|
|
154
|
+
if (match === null)
|
|
155
|
+
return undefined;
|
|
156
|
+
return { change: match[1], phase: match[2] };
|
|
157
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { assertSafeWorkspaceRelativePath, isGeneratedByVgxness, } from './workspace-port.js';
|
|
2
|
+
export class ObsidianMcpWorkspacePort {
|
|
3
|
+
options;
|
|
4
|
+
workspaceRoot;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.workspaceRoot = options.workspaceRoot ?? 'VGXNESS';
|
|
8
|
+
}
|
|
9
|
+
async status() {
|
|
10
|
+
try {
|
|
11
|
+
await this.options.client.callTool('obsidian_list_notes', { path: this.workspaceRoot, recursive: false });
|
|
12
|
+
return {
|
|
13
|
+
kind: 'knowledge-workspace-status',
|
|
14
|
+
status: 'available',
|
|
15
|
+
mode: 'obsidian-mcp',
|
|
16
|
+
workspaceRoot: this.workspaceRoot,
|
|
17
|
+
diagnostics: [],
|
|
18
|
+
safety: { readOnly: true, writesNotes: false, deletesNotes: false, executesObsidianCommands: false },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
return {
|
|
23
|
+
kind: 'knowledge-workspace-status',
|
|
24
|
+
status: 'unavailable',
|
|
25
|
+
mode: 'obsidian-mcp',
|
|
26
|
+
workspaceRoot: this.workspaceRoot,
|
|
27
|
+
diagnostics: [`Obsidian MCP unavailable: ${cause instanceof Error ? cause.message : String(cause)}`],
|
|
28
|
+
safety: { readOnly: true, writesNotes: false, deletesNotes: false, executesObsidianCommands: false },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async listNotes(input) {
|
|
33
|
+
const path = assertSafeWorkspaceRelativePath(input.pathPrefix, this.workspaceRoot);
|
|
34
|
+
const result = await this.options.client.callTool('obsidian_list_notes', { path, recursive: input.recursive ?? true });
|
|
35
|
+
return extractNoteRefs(result);
|
|
36
|
+
}
|
|
37
|
+
async getNote(input) {
|
|
38
|
+
const path = assertSafeWorkspaceRelativePath(input.path, this.workspaceRoot);
|
|
39
|
+
const result = await this.options.client.callTool('obsidian_get_note', { path, format: 'content' });
|
|
40
|
+
return { path, content: extractContent(result) };
|
|
41
|
+
}
|
|
42
|
+
async writeGeneratedNote(input) {
|
|
43
|
+
const path = assertSafeWorkspaceRelativePath(input.path, this.workspaceRoot);
|
|
44
|
+
const existing = await this.readExisting(path);
|
|
45
|
+
if (existing !== undefined && input.overwriteGeneratedOnly && !isGeneratedByVgxness(existing.content)) {
|
|
46
|
+
throw new Error(`Refusing to overwrite non-generated Obsidian note: ${path}`);
|
|
47
|
+
}
|
|
48
|
+
const result = await this.options.client.callTool('obsidian_write_note', { path, content: input.content, overwrite: existing !== undefined });
|
|
49
|
+
return extractWriteResult(result, path, existing === undefined);
|
|
50
|
+
}
|
|
51
|
+
async patchNote(input) {
|
|
52
|
+
const path = assertSafeWorkspaceRelativePath(input.path, this.workspaceRoot);
|
|
53
|
+
const result = await this.options.client.callTool('obsidian_patch_note', {
|
|
54
|
+
path,
|
|
55
|
+
section: input.section,
|
|
56
|
+
operation: input.operation,
|
|
57
|
+
content: input.content,
|
|
58
|
+
});
|
|
59
|
+
return extractWriteResult(result, path, false);
|
|
60
|
+
}
|
|
61
|
+
async openInUi(input) {
|
|
62
|
+
const path = assertSafeWorkspaceRelativePath(input.path, this.workspaceRoot);
|
|
63
|
+
await this.options.client.callTool('obsidian_open_in_ui', { path, failIfMissing: true });
|
|
64
|
+
}
|
|
65
|
+
async readExisting(path) {
|
|
66
|
+
try {
|
|
67
|
+
return await this.getNote({ path });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function extractContent(result) {
|
|
75
|
+
if (typeof result === 'string')
|
|
76
|
+
return result;
|
|
77
|
+
if (isRecord(result)) {
|
|
78
|
+
if (typeof result.content === 'string')
|
|
79
|
+
return result.content;
|
|
80
|
+
if (typeof result.result === 'string')
|
|
81
|
+
return result.result;
|
|
82
|
+
const note = result.note;
|
|
83
|
+
if (isRecord(note) && typeof note.content === 'string')
|
|
84
|
+
return note.content;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(result);
|
|
87
|
+
}
|
|
88
|
+
function extractNoteRefs(result) {
|
|
89
|
+
const entries = isRecord(result) && Array.isArray(result.entries) ? result.entries : Array.isArray(result) ? result : [];
|
|
90
|
+
return entries.flatMap((entry) => {
|
|
91
|
+
if (!isRecord(entry) || typeof entry.path !== 'string')
|
|
92
|
+
return [];
|
|
93
|
+
const type = entry.type === 'directory' ? 'directory' : 'file';
|
|
94
|
+
return [{ path: entry.path, type }];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function extractWriteResult(result, path, defaultCreated) {
|
|
98
|
+
if (!isRecord(result))
|
|
99
|
+
return { path, created: defaultCreated };
|
|
100
|
+
const output = { path, created: typeof result.created === 'boolean' ? result.created : defaultCreated };
|
|
101
|
+
if (typeof result.previousSizeInBytes === 'number')
|
|
102
|
+
output.previousSizeInBytes = result.previousSizeInBytes;
|
|
103
|
+
if (typeof result.currentSizeInBytes === 'number')
|
|
104
|
+
output.currentSizeInBytes = result.currentSizeInBytes;
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return typeof value === 'object' && value !== null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function assertSafeWorkspaceRelativePath(path, workspaceRoot = 'VGXNESS') {
|
|
2
|
+
const normalized = normalizeWorkspacePath(path);
|
|
3
|
+
const normalizedRoot = normalizeWorkspacePath(workspaceRoot);
|
|
4
|
+
if (normalizedRoot === '' || normalizedRoot === '.' || normalizedRoot.includes('..'))
|
|
5
|
+
throw new Error('Invalid workspace root');
|
|
6
|
+
if (normalized !== normalizedRoot && !normalized.startsWith(`${normalizedRoot}/`)) {
|
|
7
|
+
throw new Error(`Path must stay under ${normalizedRoot}/: ${path}`);
|
|
8
|
+
}
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
export function isGeneratedByVgxness(content) {
|
|
12
|
+
return /^---[\s\S]*?^generated_by:\s*vgxness\s*$/m.test(content);
|
|
13
|
+
}
|
|
14
|
+
export function normalizeWorkspacePath(path) {
|
|
15
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/').trim();
|
|
16
|
+
const parts = normalized.split('/').filter((part) => part.length > 0 && part !== '.');
|
|
17
|
+
if (parts.some((part) => part === '..'))
|
|
18
|
+
throw new Error(`Path traversal is not allowed: ${path}`);
|
|
19
|
+
return parts.join('/');
|
|
20
|
+
}
|
|
@@ -470,7 +470,7 @@ function delegationCheck(config) {
|
|
|
470
470
|
const missing = vgxnessOpenCodeSddSubagents.filter((name) => permission[name] !== 'allow');
|
|
471
471
|
if (missing.length > 0)
|
|
472
472
|
return { id: 'opencode-delegation-permissions', status: 'fail', detail: `Manager cannot delegate to expected subagents: ${missing.join(', ')}.` };
|
|
473
|
-
return { id: 'opencode-delegation-permissions', status: 'pass', detail: 'Manager task permission is deny-by-default and allows exact capability subagents
|
|
473
|
+
return { id: 'opencode-delegation-permissions', status: 'pass', detail: 'Manager task permission is deny-by-default and allows exact canonical capability subagents only.' };
|
|
474
474
|
}
|
|
475
475
|
function promptContractCheck(config, expected) {
|
|
476
476
|
const agents = isRecord(config?.agent) ? config.agent : undefined;
|
|
@@ -144,7 +144,6 @@ export function createCanonicalOpenCodeSddTaskPermissions() {
|
|
|
144
144
|
return Object.fromEntries([
|
|
145
145
|
['*', 'deny'],
|
|
146
146
|
...canonicalCapabilitySubagentNames.map((name) => [name, 'allow']),
|
|
147
|
-
...Object.keys(canonicalCapabilitySubagentLegacyAliases).map((name) => [name, 'allow']),
|
|
148
147
|
]);
|
|
149
148
|
}
|
|
150
149
|
export function createCanonicalOpenCodeSddMcpToolPermissions() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,6 +20,9 @@ Areas:
|
|
|
20
20
|
verification plan --type docs-only|test-only|cli|mcp|sdd-storage|provider-setup|package-release|workflow-runs|code-context [--json]
|
|
21
21
|
verification report save --project <name> --change <id> --file <report.json> [--db <path>] [--json]
|
|
22
22
|
verification report get --project <name> --change <id> [--db <path>] [--json]
|
|
23
|
+
knowledge status [--workspace obsidian-mcp|filesystem] [--vault <path>] [--json]
|
|
24
|
+
knowledge init [--workspace obsidian-mcp|filesystem] [--vault <path>] [--json]
|
|
25
|
+
knowledge export --project <name> [--workspace obsidian-mcp|filesystem] [--vault <path>] [--kind docs|memory|sdd|canvas|all] [--limit <n>] [--db <path>]
|
|
23
26
|
Status answers "where am I?" with the human front-door cockpit.
|
|
24
27
|
Next answers "what should I do now?" with a shorter next-action view.
|
|
25
28
|
Resume answers "how do I continue interrupted work?" with run inspection guidance.
|
|
@@ -29,6 +32,7 @@ Areas:
|
|
|
29
32
|
Doctor defaults to human-readable output; pass --json for automation.
|
|
30
33
|
Verification plans are read-only recommendations only; they never execute commands, write provider config, persist results, mutate SDD artifacts, or infer acceptance.
|
|
31
34
|
Verification reports are manual file-only SDD verify draft artifacts; save/get never execute commands, contact providers, write provider config, or auto-accept verification.
|
|
35
|
+
Knowledge commands project SQLite/repository state into an Obsidian workspace. Obsidian MCP is the preferred host transport; standalone CLI export fails fast without an MCP-capable host and only writes filesystem fallback when --workspace filesystem --vault <path> is explicit.
|
|
32
36
|
|
|
33
37
|
agents register --project <name> --name <name> --description <text> --instructions <text>
|
|
34
38
|
agents register --file <agent.json>
|
|
@@ -69,7 +73,7 @@ Areas:
|
|
|
69
73
|
mcp doctor [--db <path>] [--project <name>] [--change <id>] [--timeout-ms <ms>]
|
|
70
74
|
MCP setup preview is read-only; it does not install or write .opencode/, .claude/, or provider config.
|
|
71
75
|
Without --db, MCP install and setup commands use the vgxness global default database; pass --db .vgx/memory.sqlite for project-local compatibility.
|
|
72
|
-
OpenCode install defaults to user-global scope and installs mcp.vgxness plus top-level permission.bash=ask, vgxness-manager with bash=deny and native repo tools disabled, and hidden capability agents such as vgxness-explore/vgxness-plan/vgxness-verify with
|
|
76
|
+
OpenCode install defaults to user-global scope and installs mcp.vgxness plus top-level permission.bash=ask, vgxness-manager with bash=deny and native repo tools disabled, and hidden capability agents such as vgxness-explore/vgxness-plan/vgxness-verify with canonical task permissions only; use --mcp-only for legacy MCP-only config.
|
|
73
77
|
Use --overwrite-vgxness (alias --reinstall) to reinstall only VGXNESS-managed OpenCode entries while preserving unrelated config; --yes is still required to write.
|
|
74
78
|
It writes only after --yes. VGX-managed provider configuration is user-global only for OpenCode and Claude; the OpenCode target is $HOME/.config/opencode/opencode.json.
|
|
75
79
|
Project/local provider files are external/manual diagnostics and will not be written by VGXNESS. Plans are read-only; applies refuse unsafe existing user-global config and create backups before merge.
|
|
@@ -2,6 +2,7 @@ export { runCodeContextCommand } from './code-context-dispatcher.js';
|
|
|
2
2
|
export { runAgentCommand, runSkillCommand, runSubagentCommand } from './agent-skill-dispatcher.js';
|
|
3
3
|
export { runDefaultInteractiveEntrypoint } from './interactive-entrypoint-dispatcher.js';
|
|
4
4
|
export { runDoctorAliasCommand, runMcpDoctorCommand, runMcpDoctorOpenCodeCommand, runMcpInstallCommand, runMcpSetupCommand } from './mcp-dispatcher.js';
|
|
5
|
+
export { runKnowledgeWorkspaceCommand } from './knowledge-workspace-dispatcher.js';
|
|
5
6
|
export { runMemoryCommand, runMemoryImportCommand, runOpenCodeCommand, runOrchestratorCommand, runSddCommand } from './memory-sdd-dispatcher.js';
|
|
6
7
|
export { runApprovalsCommand, runPermissionsCommand, runRunsCommand } from './run-permission-dispatcher.js';
|
|
7
8
|
export { applySetupPlanInput, collectRunDetails, collectRunInsights, createSetupLifecycleService, promptSetupWizard, readSetupStatus, runHomeTuiCommand, runInitCommand, runSetupApplyCommand, runSetupBackupCommand, runSetupLifecycleCommand, runSetupPlanCommand, runSetupReinstallApplyCommand, runSetupReinstallCommand, runSetupRollbackCommand, setupMcpEvidence, setupPlanInputFromFlags, } from './setup-dispatcher.js';
|