archgraph-argo 0.10.0 → 0.10.3

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 CHANGED
@@ -26,7 +26,7 @@ npm install -g archgraph-argo
26
26
  argo-deploy
27
27
  ```
28
28
 
29
- Done — the ARGO toolchain, skills, and rules are deployed, and the `argo` MCP server is registered automatically in **GitHub Copilot**, **Cursor**, and **OpenCode**.
29
+ Done — the ARGO toolchain, skills, and rules are deployed, and the `argo` MCP server is registered automatically in **GitHub Copilot**, **Cursor**, **OpenCode**, and **DeepSeek Harness** (dsh).
30
30
 
31
31
  > Semantic (Graph RAG) queries also need **Neo4j** and a **vector engine** configured in
32
32
  > `~/.argo/.env`; everything else works out of the box.
@@ -18,6 +18,7 @@ const {
18
18
  getWorkspaceRoot,
19
19
  hasStaticWorkspace,
20
20
  resolveArgoPath,
21
+ resolveCallWorkspaceRoot,
21
22
  setMcpWorkspaceRoots,
22
23
  } = require('./argo-paths.js');
23
24
  const canonicalSemanticInitStorage = new AsyncLocalStorage();
@@ -304,6 +305,25 @@ const TOOLS = [
304
305
  },
305
306
  ];
306
307
 
308
+ // Every tool accepts an optional per-call `workspaceRoot` (absolute path,
309
+ // honored only when listed in ARGO_WORKSPACE_ROOTS). The DeepSeek Harness
310
+ // bridge injects the current session's workspace directory here automatically.
311
+ // (tools/list dedupes with systemArchitectureMcp/validatorMcp tools, which
312
+ // carry the same field; this covers the local-only rows like initializeWorkspace.)
313
+ const WORKSPACE_ROOT_PARAM = Object.freeze({
314
+ type: 'string',
315
+ description:
316
+ 'Optional absolute workspace root for this call. Honored only when listed in ARGO_WORKSPACE_ROOTS; otherwise the server launch directory is used.',
317
+ });
318
+ for (const tool of TOOLS) {
319
+ const inputSchema = tool && tool.inputSchema;
320
+ if (inputSchema && inputSchema.type === 'object' && inputSchema.properties) {
321
+ if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, 'workspaceRoot')) {
322
+ inputSchema.properties.workspaceRoot = WORKSPACE_ROOT_PARAM;
323
+ }
324
+ }
325
+ }
326
+
307
327
  function intentElementContextInputSchema() {
308
328
  return {
309
329
  type: 'object',
@@ -374,14 +394,20 @@ function resolveWorkspaceRoot() {
374
394
  return getWorkspaceRoot();
375
395
  }
376
396
 
397
+ function resolveWorkspaceRoot(args) {
398
+ // Per-call workspaceRoot override (honored only when in the
399
+ // ARGO_WORKSPACE_ROOTS allowlist); defaults to the launch-directory root.
400
+ return resolveCallWorkspaceRoot(args);
401
+ }
402
+
377
403
  async function callTool(name, args = {}, progressToken = null, dependencies = undefined) {
378
- loadRepositoryArgoEnvironment(resolveWorkspaceRoot());
404
+ loadRepositoryArgoEnvironment(resolveWorkspaceRoot(args));
379
405
  if (name === 'initializeWorkspace') {
380
- const workspace = await initializeWorkspace(resolveWorkspaceRoot());
406
+ const workspace = await initializeWorkspace(resolveWorkspaceRoot(args));
381
407
  const composition = canonicalSemanticInitStorage.getStore()
382
408
  || systemArchitectureMcp.createDefaultCanonicalSemanticInitComposition();
383
409
  const semanticLifecycle = await runCanonicalSemanticInit(composition, {
384
- repositoryRoot: resolveWorkspaceRoot(),
410
+ repositoryRoot: resolveWorkspaceRoot(args),
385
411
  workspace,
386
412
  });
387
413
  return toolResult({
@@ -121,6 +121,48 @@ function normalizeRelativePath(value) {
121
121
  return String(value == null ? '' : value).replace(/\\/g, '/').replace(/^\/+/, '');
122
122
  }
123
123
 
124
+ /**
125
+ * Resolve the workspace root for one tool call.
126
+ *
127
+ * Defaults to the launch-directory root (`getWorkspaceRoot()`). A per-call
128
+ * `workspaceRoot` argument (absolute path) is honored only when it appears in
129
+ * the `ARGO_WORKSPACE_ROOTS` allowlist (semicolon or comma separated absolute
130
+ * paths). Without an allowlist the override is ignored, so existing
131
+ * deployments keep launch-directory resolution with zero behavior change.
132
+ * This powers the DeepSeek Harness bridge, which injects the current session's
133
+ * workspace directory into every call so one dsh instance can follow the
134
+ * workspace the user switched to.
135
+ *
136
+ * @param {object} [args] - tool call arguments (may carry `workspaceRoot`).
137
+ * @returns {string} resolved absolute workspace root.
138
+ * @throws when the requested root is not in the allowlist.
139
+ */
140
+ function resolveCallWorkspaceRoot(args = {}) {
141
+ const requested =
142
+ typeof args === 'object' && args !== null && typeof args.workspaceRoot === 'string'
143
+ ? String(args.workspaceRoot).trim()
144
+ : '';
145
+ if (requested === '') {
146
+ return getWorkspaceRoot();
147
+ }
148
+ const allowlist = (process.env.ARGO_WORKSPACE_ROOTS || '')
149
+ .split(/[;,]/)
150
+ .map((entry) => entry.trim())
151
+ .filter(Boolean)
152
+ .map((entry) => path.resolve(entry));
153
+ if (allowlist.length === 0) {
154
+ return getWorkspaceRoot();
155
+ }
156
+ const resolved = path.resolve(requested);
157
+ const allowed = allowlist.some(
158
+ (root) => resolved === root || resolved.startsWith(root + path.sep),
159
+ );
160
+ if (!allowed) {
161
+ throw new Error(`workspaceRoot not in ARGO_WORKSPACE_ROOTS allowlist: ${requested}`);
162
+ }
163
+ return resolved;
164
+ }
165
+
124
166
  module.exports = {
125
167
  getArgoRoot,
126
168
  getArgoEnvPath,
@@ -128,6 +170,7 @@ module.exports = {
128
170
  hasStaticWorkspace,
129
171
  normalizeRelativePath,
130
172
  resolveArgoPath,
173
+ resolveCallWorkspaceRoot,
131
174
  resolveWorkspacePath,
132
175
  setMcpWorkspaceRoots,
133
176
  };
@@ -5,7 +5,7 @@ const crypto = require('node:crypto');
5
5
 
6
6
  const {
7
7
  getArgoRoot,
8
- getWorkspaceRoot,
8
+ resolveCallWorkspaceRoot,
9
9
  } = require('./argo-paths.js');
10
10
 
11
11
  const DEFAULT_GRAPH_PATH = 'design/KG/SystemArchitecture.json';
@@ -381,6 +381,23 @@ const TOOLS = [
381
381
  },
382
382
  ];
383
383
 
384
+ // Every tool accepts an optional per-call `workspaceRoot` (absolute path,
385
+ // honored only when listed in ARGO_WORKSPACE_ROOTS). The DeepSeek Harness
386
+ // bridge injects the current session's workspace directory here automatically.
387
+ const WORKSPACE_ROOT_PARAM = Object.freeze({
388
+ type: 'string',
389
+ description:
390
+ 'Optional absolute workspace root for this call. Honored only when listed in ARGO_WORKSPACE_ROOTS; otherwise the server launch directory is used.',
391
+ });
392
+ for (const tool of TOOLS) {
393
+ const inputSchema = tool && tool.inputSchema;
394
+ if (inputSchema && inputSchema.type === 'object' && inputSchema.properties) {
395
+ if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, 'workspaceRoot')) {
396
+ inputSchema.properties.workspaceRoot = WORKSPACE_ROOT_PARAM;
397
+ }
398
+ }
399
+ }
400
+
384
401
  function intentElementContextInputSchema() {
385
402
  return {
386
403
  type: 'object',
@@ -448,13 +465,15 @@ function mutationInputSchema() {
448
465
  };
449
466
  }
450
467
 
451
- function resolveWorkspaceRoot() {
452
- return getWorkspaceRoot();
468
+ function resolveWorkspaceRoot(args) {
469
+ // Per-call workspaceRoot override (honored only when in the
470
+ // ARGO_WORKSPACE_ROOTS allowlist); defaults to the launch-directory root.
471
+ return resolveCallWorkspaceRoot(args);
453
472
  }
454
473
 
455
474
  function initializeWorkspace(request) {
456
475
  return require('./argo-mcp-server.js').initializeWorkspace(
457
- request && request.repositoryRoot ? request.repositoryRoot : resolveWorkspaceRoot(),
476
+ request && request.repositoryRoot ? request.repositoryRoot : resolveWorkspaceRoot(request),
458
477
  );
459
478
  }
460
479
 
@@ -508,7 +527,7 @@ function readJson(filePath, label) {
508
527
  }
509
528
 
510
529
  async function loadContext(args = {}) {
511
- const workspaceRoot = resolveWorkspaceRoot();
530
+ const workspaceRoot = resolveWorkspaceRoot(args);
512
531
  const graphPath = resolveWorkspacePath(workspaceRoot, args.architecturePath || DEFAULT_GRAPH_PATH);
513
532
  const schemaPath = resolveSchemaPath(workspaceRoot);
514
533
  const context = {
@@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile);
9
9
  const {
10
10
  getArgoRoot,
11
11
  getWorkspaceRoot,
12
+ resolveCallWorkspaceRoot,
12
13
  } = require('./argo-paths.js');
13
14
 
14
15
  const HANDOFF_STAGES = ['intent-to-implementation', 'implementation-to-coding'];
@@ -85,8 +86,27 @@ const TOOLS = [
85
86
  },
86
87
  ];
87
88
 
88
- function resolveWorkspaceRoot() {
89
- return getWorkspaceRoot();
89
+ // Every tool accepts an optional per-call `workspaceRoot` (absolute path,
90
+ // honored only when listed in ARGO_WORKSPACE_ROOTS). The DeepSeek Harness
91
+ // bridge injects the current session's workspace directory here automatically.
92
+ const WORKSPACE_ROOT_PARAM = Object.freeze({
93
+ type: 'string',
94
+ description:
95
+ 'Optional absolute workspace root for this call. Honored only when listed in ARGO_WORKSPACE_ROOTS; otherwise the server launch directory is used.',
96
+ });
97
+ for (const tool of TOOLS) {
98
+ const inputSchema = tool && tool.inputSchema;
99
+ if (inputSchema && inputSchema.type === 'object' && inputSchema.properties) {
100
+ if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, 'workspaceRoot')) {
101
+ inputSchema.properties.workspaceRoot = WORKSPACE_ROOT_PARAM;
102
+ }
103
+ }
104
+ }
105
+
106
+ function resolveWorkspaceRoot(args) {
107
+ // Per-call workspaceRoot override (honored only when in the
108
+ // ARGO_WORKSPACE_ROOTS allowlist); defaults to the launch-directory root.
109
+ return resolveCallWorkspaceRoot(args);
90
110
  }
91
111
 
92
112
  function resolveScriptPath(workspaceRoot, candidates) {
@@ -231,7 +251,7 @@ function toolResult(payload) {
231
251
  }
232
252
 
233
253
  async function callTool(name, args, progressToken = null) {
234
- const workspaceRoot = resolveWorkspaceRoot();
254
+ const workspaceRoot = resolveWorkspaceRoot(args);
235
255
 
236
256
  if (name === 'validateSystemArchitecture') {
237
257
  return toolResult(await runValidatorScript(workspaceRoot, 'validateSystemArchitecture'));
package/install-argo.ps1 CHANGED
@@ -16,6 +16,10 @@ param(
16
16
  [switch]$SkipEnv,
17
17
  [switch]$SkipDeps,
18
18
  [switch]$SkipMcp,
19
+ [switch]$SkipDsh,
20
+ [string]$DshHome = "$env:USERPROFILE\.dsh",
21
+ [string]$DshCwd = '',
22
+ [string]$DshWorkspaces = '',
19
23
  [string]$McpPath
20
24
  )
21
25
 
@@ -235,70 +239,505 @@ function Add-AgentsRule {
235
239
  }
236
240
  }
237
241
 
242
+ # ---- DeepSeek Harness (dsh) conversion helpers ----
243
+ # The ArchGraph repository keeps ONE source file per artifact (rule / skill /
244
+ # mcp / plugin / agent). The targets below convert that single source into the
245
+ # DeepSeek Harness shape at deploy time, exactly like Convert-AgentFile and
246
+ # Convert-RuleFile do for Cursor / OpenCode.
247
+
248
+ function Get-MarkdownBody {
249
+ # Strip a leading YAML frontmatter block (--- ... ---) from markdown text.
250
+ # DeepSeek Harness injects AGENTS.md-style instruction files verbatim, so
251
+ # the frontmatter must not reach the model prompt.
252
+ param([string]$Content)
253
+ $m = [regex]::Match($Content, '(?s)^---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)$')
254
+ if (-not $m.Success) { return $Content }
255
+ return $m.Groups[2].Value
256
+ }
257
+
258
+ function Get-WakeupGuideline {
259
+ # Extract the <WakeupGuideline>...</WakeupGuideline> block from the rule
260
+ # text, so the DSH wakeup plugin is generated from the same source the
261
+ # Copilot / Cursor / OpenCode rules use.
262
+ param([string]$RuleText)
263
+ $m = [regex]::Match($RuleText, '(?s)<WakeupGuideline>(.*?)</WakeupGuideline>')
264
+ if (-not $m.Success) { return '' }
265
+ return $m.Groups[1].Value.Trim()
266
+ }
267
+
268
+ function Write-DshAgentRule {
269
+ # Merge the frontmatter-stripped ArchGraph rule into ~/.dsh/AGENTS.md,
270
+ # replacing the previous ArchGraph block while preserving surrounding user
271
+ # content. The rule body is injected verbatim - no adapter prose, so the
272
+ # working prompt stays identical to the Copilot / Cursor / OpenCode rules.
273
+ param(
274
+ [string]$DshHome,
275
+ [string]$RuleText
276
+ )
277
+ $ruleContent = Get-MarkdownBody -Content $RuleText
278
+ $dest = Join-Path $DshHome 'AGENTS.md'
279
+ $marker = '<WakeupGuideline>'
280
+ New-Item -ItemType Directory -Force -Path $DshHome | Out-Null
281
+ if (Test-Path $dest) {
282
+ $existing = Get-Content $dest -Raw -Encoding UTF8
283
+ if ($existing -like "*$marker*") {
284
+ $endTag = '</ToolsGuideline>'
285
+ $startIdx = $existing.IndexOf($marker)
286
+ if ($startIdx -lt 0) { $startIdx = 0 }
287
+ $endIdx = $existing.IndexOf($endTag, $startIdx)
288
+ $before = $existing.Substring(0, $startIdx).TrimEnd()
289
+ if ($endIdx -lt 0) {
290
+ $combined = $before
291
+ if ($combined.Length -gt 0) { $combined += "`n`n" }
292
+ $combined += $ruleContent
293
+ } else {
294
+ $after = $existing.Substring($endIdx + $endTag.Length)
295
+ $combined = $before
296
+ if ($combined.Length -gt 0) { $combined += "`n`n" }
297
+ $combined += $ruleContent
298
+ if ($after.Length -gt 0) { $combined += $after }
299
+ }
300
+ [System.IO.File]::WriteAllText($dest, $combined, (New-Object System.Text.UTF8Encoding $false))
301
+ } else {
302
+ $combined = $existing.TrimEnd() + "`n`n" + $ruleContent
303
+ [System.IO.File]::WriteAllText($dest, $combined, (New-Object System.Text.UTF8Encoding $false))
304
+ }
305
+ } else {
306
+ [System.IO.File]::WriteAllText($dest, $ruleContent, (New-Object System.Text.UTF8Encoding $false))
307
+ }
308
+ Write-Host " DSH rule installed -> $dest"
309
+ }
310
+
311
+ function Write-DshManagedBlock {
312
+ # Text-level upsert of a marker-delimited block into a file (used for the
313
+ # managed rows in ~/.dsh/cordis.patch.yml): replaces the previous managed
314
+ # block and preserves surrounding content. No YAML dependency needed.
315
+ param(
316
+ [string]$Path,
317
+ [string]$Block,
318
+ [string]$MarkerStart,
319
+ [string]$MarkerEnd
320
+ )
321
+ New-Item -ItemType Directory -Force -Path (Split-Path $Path) | Out-Null
322
+ $existing = if (Test-Path $Path) { Get-Content $Path -Raw -Encoding UTF8 } else { '' }
323
+ $startIdx = $existing.IndexOf($MarkerStart)
324
+ $endIdx = if ($startIdx -ge 0) { $existing.IndexOf($MarkerEnd, $startIdx) } else { -1 }
325
+ if ($startIdx -ge 0 -and $endIdx -ge 0) {
326
+ $endIdx += $MarkerEnd.Length
327
+ $combined = $existing.Substring(0, $startIdx).TrimEnd()
328
+ if ($combined.Length -gt 0) { $combined += "`n`n" }
329
+ $combined += $Block
330
+ $after = $existing.Substring($endIdx).TrimStart()
331
+ if ($after.Length -gt 0) { $combined += "`n`n" + $after }
332
+ [System.IO.File]::WriteAllText($Path, $combined, (New-Object System.Text.UTF8Encoding $false))
333
+ } else {
334
+ $combined = $existing.TrimEnd()
335
+ if ($combined.Length -gt 0) { $combined += "`n`n" }
336
+ $combined += $Block + "`n"
337
+ [System.IO.File]::WriteAllText($Path, $combined, (New-Object System.Text.UTF8Encoding $false))
338
+ }
339
+ }
340
+
341
+ function New-DshWakeupPlugin {
342
+ # Generate the DSH wakeup-gate Cordis plugin under ~/.dsh/plugins from the
343
+ # <WakeupGuideline> block of the rule file. This is the DeepSeek Harness
344
+ # equivalent of the OpenCode hook plugin argo/plugins/argo-wakeup.js: it
345
+ # registers the gate as the first system-prompt section after the harness
346
+ # identity (order -100), before the deployment persona (order 0).
347
+ param(
348
+ [string]$DshHome,
349
+ [string]$RuleText
350
+ )
351
+ $gate = Get-WakeupGuideline -RuleText $RuleText
352
+ if (-not $gate) {
353
+ Write-Warning ' <WakeupGuideline> not found in the rule file; DSH wakeup plugin skipped.'
354
+ return $null
355
+ }
356
+ $dir = Join-Path (Join-Path $DshHome 'plugins') 'dsh-argo-wakeup'
357
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
358
+ $gateJson = $gate | ConvertTo-Json
359
+ $plugin = @"
360
+ // dsh-argo-wakeup - generated by install-argo.ps1 from
361
+ // argo/rules/archgraph.instructions.md (single source of truth: the rule file;
362
+ // this file is a deployment artifact, do not edit by hand).
363
+ // DeepSeek Harness equivalent of the OpenCode hook argo/plugins/argo-wakeup.js:
364
+ // registers the unconditional wakeup gate as the first system-prompt section
365
+ // after the harness identity, so every session identifies its Business Actor
366
+ // through the argo MCP server before responding.
367
+ export const name = 'dsh-argo-wakeup'
368
+ export const inject = ['systemPrompt']
369
+ const WAKEUP_GATE = $gateJson
370
+ export function apply(ctx) {
371
+ ctx.effect(() => ctx.systemPrompt.section({
372
+ name: 'argo:wakeup',
373
+ order: -90,
374
+ text: WAKEUP_GATE,
375
+ }), 'argo:wakeup.section()')
376
+ }
377
+ "@
378
+ $indexPath = Join-Path $dir 'index.js'
379
+ [System.IO.File]::WriteAllText($indexPath, $plugin, (New-Object System.Text.UTF8Encoding $false))
380
+ Write-Host " DSH wakeup plugin generated -> $indexPath"
381
+ return $indexPath
382
+ }
383
+
384
+ function New-DshWorkspaceBridge {
385
+ # Generate the DSH workspace bridge plugin under ~/.dsh/plugins. It
386
+ # connects directly to the argo MCP server (no dsh-mcp-client row needed)
387
+ # with a zero-dependency minimal MCP stdio client, registers every tool as
388
+ # mcp__argo__* and injects the current session's workspace directory
389
+ # (SessionHeader.cwd) as the per-call `workspaceRoot`, so ONE dsh instance
390
+ # follows whichever workspace the user switched to - no model-visible
391
+ # parameters, no internal tool names, no restart. The argo server honors
392
+ # workspaceRoot only when listed in ARGO_WORKSPACE_ROOTS (install-argo.ps1
393
+ # -DshWorkspaces).
394
+ param(
395
+ [string]$DshHome
396
+ )
397
+ $dir = Join-Path (Join-Path $DshHome 'plugins') 'dsh-argo-workspace'
398
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
399
+ $plugin = @'
400
+ // dsh-argo-workspace - generated by install-argo.ps1 (single source of truth:
401
+ // the installer; this file is a deployment artifact, do not edit by hand).
402
+ //
403
+ // Direct MCP bridge to the argo server for DeepSeek Harness. Registers every
404
+ // argo tool as mcp__argo__* and injects the current session's workspace
405
+ // directory (SessionHeader.cwd) as the per-call `workspaceRoot` argument, so
406
+ // one dsh instance follows whichever workspace the user switched to - the
407
+ // model sees no extra parameters and no internal tool names. The argo server
408
+ // honors workspaceRoot only when the workspace is listed in its
409
+ // ARGO_WORKSPACE_ROOTS allowlist.
410
+ //
411
+ // Zero dependencies on purpose: implements the minimal MCP stdio client
412
+ // (JSON-RPC 2.0, one JSON object per line) with Node built-ins only, so the
413
+ // plugin runs from any DSH layout without resolving @modelcontextprotocol/sdk.
414
+ import { spawn } from 'node:child_process'
415
+ import readline from 'node:readline'
416
+
417
+ export const name = 'dsh-argo-workspace'
418
+ export const inject = ['tools']
419
+
420
+ /** Minimal MCP stdio client over the argo server process. */
421
+ function createArgoClient(serverPath, env, cwd) {
422
+ const child = spawn('node', [serverPath], {
423
+ stdio: ['pipe', 'pipe', 'pipe'],
424
+ env,
425
+ ...(cwd ? { cwd } : {}),
426
+ })
427
+ const pending = new Map()
428
+ let nextId = 1
429
+ readline.createInterface({ input: child.stdout }).on('line', (line) => {
430
+ let message
431
+ try { message = JSON.parse(line) } catch { return }
432
+ if (message && typeof message.id === 'number' && pending.has(message.id)) {
433
+ const { resolve, reject } = pending.get(message.id)
434
+ pending.delete(message.id)
435
+ if (message.error) reject(new Error(JSON.stringify(message.error)))
436
+ else resolve(message.result)
437
+ }
438
+ })
439
+ child.stderr.on('data', () => {}) // drain; the argo server logs to stderr
440
+ const request = (method, params) => new Promise((resolve, reject) => {
441
+ const id = nextId++
442
+ pending.set(id, { resolve, reject })
443
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n')
444
+ })
445
+ return {
446
+ request,
447
+ notify: (method, params) => {
448
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
449
+ },
450
+ close: () => child.kill(),
451
+ }
452
+ }
453
+
454
+ /** Connect, list tools, register them, and keep the client until disposal. */
455
+ export async function apply(ctx, config) {
456
+ const serverPath = config.serverPath
457
+ const workspaces = Array.isArray(config.workspaces) ? config.workspaces : []
458
+ const env = { ...process.env }
459
+ if (workspaces.length > 0) env.ARGO_WORKSPACE_ROOTS = workspaces.join(';')
460
+ const client = createArgoClient(serverPath, env, config.cwd)
461
+ ctx.effect(() => () => client.close(), 'dsh-argo-workspace.dispose()')
462
+
463
+ let tools = []
464
+ try {
465
+ await client.request('initialize', {
466
+ protocolVersion: '2024-11-05',
467
+ capabilities: {},
468
+ clientInfo: { name: 'dsh-argo-workspace', version: '0.1.0' },
469
+ })
470
+ client.notify('notifications/initialized', {})
471
+ const listed = await client.request('tools/list', {})
472
+ tools = (listed && Array.isArray(listed.tools)) ? listed.tools : []
473
+ } catch (error) {
474
+ console.warn(`[dsh-argo-workspace] failed to connect to the argo MCP server (${serverPath}): ${error && error.message ? error.message : error}`)
475
+ return
476
+ }
477
+
478
+ const disposers = []
479
+ for (const tool of tools) {
480
+ const publicName = 'mcp__argo__' + tool.name
481
+ disposers.push(ctx.tools.register({
482
+ name: publicName,
483
+ description: tool.description ?? '',
484
+ parameters: tool.inputSchema,
485
+ output: {
486
+ schema: {
487
+ type: 'object',
488
+ properties: { content: { type: 'array', items: {} } },
489
+ required: ['content'],
490
+ additionalProperties: false,
491
+ },
492
+ },
493
+ execute: async (args, exec) => {
494
+ const sessionCwd = exec && exec.agent && exec.agent.session
495
+ ? exec.agent.session.requestHeader().cwd
496
+ : undefined
497
+ const injected = { ...(args && typeof args === 'object' ? args : {}) }
498
+ if (typeof sessionCwd === 'string' && sessionCwd !== '') {
499
+ injected.workspaceRoot = sessionCwd
500
+ }
501
+ if (exec && exec.signal && exec.signal.aborted) {
502
+ throw new Error('aborted')
503
+ }
504
+ const result = await client.request('tools/call', {
505
+ name: tool.name,
506
+ arguments: injected,
507
+ })
508
+ const text = Array.isArray(result.content)
509
+ ? result.content
510
+ .map((block) => block && block.type === 'text' && typeof block.text === 'string'
511
+ ? block.text
512
+ : JSON.stringify(block))
513
+ .join('\n')
514
+ : (result.toolResult !== undefined ? JSON.stringify(result.toolResult) : '(no output)')
515
+ if (result.isError === true) throw new Error(text)
516
+ return {
517
+ content: [{ type: 'text', text }],
518
+ ...(result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}),
519
+ }
520
+ },
521
+ }))
522
+ }
523
+ ctx.effect(() => () => {
524
+ for (const dispose of disposers) dispose()
525
+ }, 'dsh-argo-workspace.tools')
526
+ }
527
+ '@
528
+ $indexPath = Join-Path $dir 'index.js'
529
+ [System.IO.File]::WriteAllText($indexPath, $plugin, (New-Object System.Text.UTF8Encoding $false))
530
+ Write-Host " DSH workspace bridge generated -> $indexPath"
531
+ return $indexPath
532
+ }
533
+
534
+ function New-DshAgentPresets {
535
+ # Generate DSH agent presets under ~/.dsh/.agent-presets/<id>/ from
536
+ # argo/agents/*.agent.md (the same single source the Copilot / Cursor /
537
+ # OpenCode agent files are converted from). persona.md is a
538
+ # frontmatter-stripped copy of the agent body; persona.js is a fixed
539
+ # self-contained row that mounts it as the session persona
540
+ # (deployment:persona section, order 0).
541
+ param(
542
+ [string]$DshHome,
543
+ [string]$AgentsSrc
544
+ )
545
+ $files = @(Get-ChildItem -Path (Join-Path $AgentsSrc '*.agent.md') -ErrorAction SilentlyContinue)
546
+ if ($files.Count -eq 0) {
547
+ Write-Warning ' no *.agent.md files found; DSH agent presets skipped.'
548
+ return
549
+ }
550
+ foreach ($file in $files) {
551
+ $content = Get-Content $file.FullName -Raw -Encoding UTF8
552
+ $m = [regex]::Match($content, '(?s)^---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)$')
553
+ $name = ''
554
+ $desc = ''
555
+ $body = $content
556
+ if ($m.Success) {
557
+ $front = $m.Groups[1].Value
558
+ $body = $m.Groups[2].Value.TrimStart("`r", "`n")
559
+ $nm = [regex]::Match($front, '(?m)^name:\s*(.*)$')
560
+ if ($nm.Success) { $name = $nm.Groups[1].Value.Trim().Trim('"').Trim("'") }
561
+ $dm = [regex]::Match($front, '(?m)^description:\s*(.*)$')
562
+ if ($dm.Success) { $desc = $dm.Groups[1].Value.Trim().Trim('"').Trim("'") }
563
+ }
564
+ $id = $file.BaseName -replace '\.agent$', ''
565
+ if (-not $name) { $name = $id }
566
+ $dir = Join-Path (Join-Path $DshHome '.agent-presets') $id
567
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
568
+
569
+ # preset.yml: display metadata (name + description).
570
+ $presetYml = "name: $name`n"
571
+ if ($desc) { $presetYml += "description: `"$($desc -replace '"', '\"')`"`n" }
572
+ [System.IO.File]::WriteAllText((Join-Path $dir 'preset.yml'), $presetYml, (New-Object System.Text.UTF8Encoding $false))
573
+
574
+ # persona.md: the agent body verbatim (single source: the .agent.md file).
575
+ [System.IO.File]::WriteAllText((Join-Path $dir 'persona.md'), $body, (New-Object System.Text.UTF8Encoding $false))
576
+
577
+ # agent.cordis.yml: mount the local persona row.
578
+ $cordisYml = @"
579
+ # ArchGraph agent preset "$name" - generated by install-argo.ps1 from
580
+ # argo/agents/$($file.Name) (single source of truth; this file is a deployment
581
+ # artifact, do not edit by hand). The persona is the frontmatter-stripped agent
582
+ # body (persona.md), mounted as the session's deployment:persona section.
583
+ - id: persona
584
+ name: './persona.js'
585
+ "@
586
+ [System.IO.File]::WriteAllText((Join-Path $dir 'agent.cordis.yml'), $cordisYml, (New-Object System.Text.UTF8Encoding $false))
587
+
588
+ # persona.js: fixed row implementation.
589
+ $personaJs = @'
590
+ // persona row for an ArchGraph agent preset (generated by install-argo.ps1).
591
+ // Loads persona.md next to this file and registers it as the
592
+ // deployment:persona system-prompt section (order 0), shadowing the
593
+ // deployment persona for the session that mounts this preset.
594
+ import { readFileSync } from 'node:fs'
595
+ import { fileURLToPath } from 'node:url'
596
+ export const name = 'persona'
597
+ export const inject = ['systemPrompt']
598
+ export function apply(ctx) {
599
+ const text = readFileSync(fileURLToPath(new URL('./persona.md', import.meta.url)), 'utf8')
600
+ ctx.effect(() => ctx.systemPrompt.section({
601
+ name: 'deployment:persona',
602
+ order: 0,
603
+ text,
604
+ }), 'persona.section()')
605
+ }
606
+ '@
607
+ [System.IO.File]::WriteAllText((Join-Path $dir 'persona.js'), $personaJs, (New-Object System.Text.UTF8Encoding $false))
608
+ Write-Host " DSH agent preset generated -> $dir"
609
+ }
610
+ }
611
+
238
612
  Write-Host '==> Deploying Argo toolchain'
239
613
 
240
614
  $schemaSrc = Join-Path $argoDir 'schema'
241
615
  $schemaDest = Join-Path $ArgoRoot 'schema'
242
- Write-Host "[1/14] argo\schema -> $schemaDest"
616
+ Write-Host "[1/19] argo\schema -> $schemaDest"
243
617
  Copy-Tree -Source $schemaSrc -Destination $schemaDest
244
618
 
245
619
  $scriptsSrc = Join-Path $argoDir 'scripts'
246
620
  $scriptsDest = Join-Path $ArgoRoot 'scripts'
247
- Write-Host "[2/14] argo\scripts -> $scriptsDest"
621
+ Write-Host "[2/19] argo\scripts -> $scriptsDest"
248
622
  Copy-Tree -Source $scriptsSrc -Destination $scriptsDest
249
623
 
250
624
  $defaultsSrc = Join-Path $argoDir 'defaults'
251
625
  $defaultsDest = Join-Path $ArgoRoot 'defaults'
252
- Write-Host "[3/14] argo\defaults -> $defaultsDest"
626
+ Write-Host "[3/19] argo\defaults -> $defaultsDest"
253
627
  Copy-Tree -Source $defaultsSrc -Destination $defaultsDest
254
628
 
255
629
  $skillSrc = Join-Path (Join-Path $argoDir 'skills') 'argo-init'
256
630
  $skillDest = Join-Path $SkillsRoot 'argo-init'
257
- Write-Host "[4/14] argo\skills\argo-init -> $skillDest"
631
+ Write-Host "[4/19] argo\skills\argo-init -> $skillDest"
258
632
  Copy-Tree -Source $skillSrc -Destination $skillDest
259
633
 
260
634
  $ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
261
635
  $ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
262
- Write-Host "[5/14] argo\rules\archgraph.instructions.md -> $ruleDest"
636
+ Write-Host "[5/19] argo\rules\archgraph.instructions.md -> $ruleDest"
263
637
  New-Item -ItemType Directory -Force -Path $PromptsRoot | Out-Null
264
638
  Copy-Item -Force -Path $ruleSrc -Destination $ruleDest
265
639
 
266
640
  $depsSrc = Join-Path $argoDir 'package.json'
267
641
  $depsDest = Join-Path $ArgoRoot 'package.json'
268
- Write-Host "[6/14] argo\package.json -> $depsDest"
642
+ Write-Host "[6/19] argo\package.json -> $depsDest"
269
643
  Copy-Item -Force -Path $depsSrc -Destination $depsDest
270
644
 
271
645
  $cursorSkillDest = Join-Path $CursorSkillsRoot 'argo-init'
272
- Write-Host "[7/14] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
646
+ Write-Host "[7/19] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
273
647
  Copy-Tree -Source $skillSrc -Destination $cursorSkillDest
274
648
 
275
649
  $openCodeSkillDest = Join-Path $OpenCodeSkillsRoot 'argo-init'
276
- Write-Host "[8/14] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
650
+ Write-Host "[8/19] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
277
651
  Copy-Tree -Source $skillSrc -Destination $openCodeSkillDest
278
652
 
279
- Write-Host "[9/14] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
653
+ Write-Host "[9/19] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
280
654
  Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
281
655
 
282
656
  $agentsSrc = Join-Path $argoDir 'agents'
283
- Write-Host "[10/14] argo\agents -> $CopilotAgentsRoot (Copilot user-level)"
657
+ Write-Host "[10/19] argo\agents -> $CopilotAgentsRoot (Copilot user-level)"
284
658
  Copy-Agents -Source $agentsSrc -Destination $CopilotAgentsRoot
285
659
 
286
- Write-Host "[11/14] argo\agents -> $CursorAgentsRoot (Cursor user-level, converted to .md)"
660
+ Write-Host "[11/19] argo\agents -> $CursorAgentsRoot (Cursor user-level, converted to .md)"
287
661
  Copy-Agents -Source $agentsSrc -Destination $CursorAgentsRoot -Target cursor
288
662
 
289
- Write-Host "[12/14] argo\agents -> $OpenCodeAgentsRoot (OpenCode user-level, converted to .md)"
663
+ Write-Host "[12/19] argo\agents -> $OpenCodeAgentsRoot (OpenCode user-level, converted to .md)"
290
664
  Copy-Agents -Source $agentsSrc -Destination $OpenCodeAgentsRoot -Target opencode
291
665
 
292
666
  $pluginsSrc = Join-Path $argoDir 'plugins'
293
- Write-Host "[13/14] argo\plugins -> $PluginsRoot (Argo opencode plugins)"
667
+ Write-Host "[13/19] argo\plugins -> $PluginsRoot (Argo opencode plugins)"
294
668
  Copy-Tree -Source $pluginsSrc -Destination $PluginsRoot
295
669
 
296
670
  $cursorRuleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
297
671
  $cursorRuleDest = Join-Path $CursorRulesRoot 'archgraph.mdc'
298
- Write-Host "[14/14] argo\rules\archgraph.instructions.md -> $cursorRuleDest (Cursor global rule, alwaysApply)"
672
+ Write-Host "[14/19] argo\rules\archgraph.instructions.md -> $cursorRuleDest (Cursor global rule, alwaysApply)"
299
673
  New-Item -ItemType Directory -Force -Path $CursorRulesRoot | Out-Null
300
674
  Convert-RuleFile -SourceFile $cursorRuleSrc -DestinationFile $cursorRuleDest
301
675
 
676
+ if ($SkipDsh) {
677
+ Write-Host 'Skipped DeepSeek Harness integration (-SkipDsh).'
678
+ } else {
679
+ $ruleSrcContent = Get-Content $ruleSrc -Raw -Encoding UTF8
680
+ $dshSkillDest = Join-Path (Join-Path $DshHome 'skills') 'argo-init'
681
+ $patchPath = Join-Path $DshHome 'cordis.patch.yml'
682
+ $argoServer = (Join-Path $ArgoRoot 'scripts\argo-mcp-server.js').Replace('\', '/')
683
+
684
+ Write-Host "[15/19] argo\rules\archgraph.instructions.md -> $DshHome\AGENTS.md (DeepSeek Harness user-global rule, frontmatter stripped)"
685
+ Write-DshAgentRule -DshHome $DshHome -RuleText $ruleSrcContent
686
+
687
+ Write-Host "[16/19] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
688
+ Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
689
+
690
+ Write-Host "[17/19] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
691
+ $wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
692
+
693
+ Write-Host "[18/19] argo-workspace + argo-wakeup rows -> $patchPath (DeepSeek Harness MCP bridge + wakeup plugin)"
694
+ # The generated dsh-argo-workspace bridge connects directly to the argo
695
+ # server (no dsh-mcp-client row), registers every tool as mcp__argo__* and
696
+ # injects the current session's workspace (SessionHeader.cwd) as the
697
+ # per-call workspaceRoot, so one dsh instance follows the workspace the
698
+ # user switched to. The server honors workspaceRoot only when the workspace
699
+ # is listed in ARGO_WORKSPACE_ROOTS (pass -DshWorkspaces "D:/a;D:/b").
700
+ $bridgeDshPath = New-DshWorkspaceBridge -DshHome $DshHome
701
+ if ($bridgeDshPath) {
702
+ $bridgeUrl = 'file:///' + (($bridgeDshPath -replace '\\', '/').TrimStart('/'))
703
+ $bridgeConfig = " config:`n serverPath: '$argoServer'`n"
704
+ if ($DshWorkspaces) {
705
+ $wsList = @($DshWorkspaces.Replace('\', '/').Split(';') | ForEach-Object { " - $_" }) -join "`n"
706
+ $bridgeConfig += " workspaces:`n" + $wsList + "`n"
707
+ }
708
+ if ($DshCwd) {
709
+ $bridgeConfig += " cwd: $($DshCwd.Replace('\', '/'))`n"
710
+ }
711
+ $rows = " - id: argo-workspace`n name: '$bridgeUrl'`n" + $bridgeConfig
712
+ } else {
713
+ $rows = ''
714
+ }
715
+ if ($wakeupDshPath) {
716
+ $pluginUrl = 'file:///' + (($wakeupDshPath -replace '\\', '/').TrimStart('/'))
717
+ $rows += " - id: argo-wakeup`n name: '$pluginUrl'`n"
718
+ }
719
+ if ($rows) {
720
+ $block = "# BEGIN ArchGraph ARGO deployment (managed by install-argo.ps1)`n- insert:`n" + $rows + "# END ArchGraph ARGO deployment"
721
+ Write-DshManagedBlock -Path $patchPath -Block $block -MarkerStart '# BEGIN ArchGraph ARGO deployment' -MarkerEnd '# END ArchGraph ARGO deployment'
722
+ }
723
+
724
+ Write-Host "[19/19] argo\agents -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
725
+ New-DshAgentPresets -DshHome $DshHome -AgentsSrc (Join-Path $argoDir 'agents')
726
+
727
+ Write-Host ' Restart `dsh web` to activate the MCP bridge and the wakeup plugin;'
728
+ Write-Host ' new sessions pick up the global rule and the argo-init skill automatically.'
729
+ if ($DshWorkspaces) {
730
+ Write-Host " Workspace following enabled: ARGO_WORKSPACE_ROOTS=$DshWorkspaces"
731
+ Write-Host ' The argo tools (mcp__argo__*) now auto-follow the workspace the user switched to'
732
+ Write-Host ' in one dsh instance - no restart needed between workspaces.'
733
+ } else {
734
+ Write-Host ' Multi-workspace: to let one dsh instance follow whichever workspace the user'
735
+ Write-Host ' switches to, re-run with -DshWorkspaces "D:/proj-a;D:/proj-b" (the argo server'
736
+ Write-Host ' only honors workspaces listed in ARGO_WORKSPACE_ROOTS). Without it the server'
737
+ Write-Host ' keeps resolving from the directory dsh was launched from.'
738
+ }
739
+ }
740
+
302
741
  if ($SkipDeps) {
303
742
  Write-Host 'Skipped dependency install (-SkipDeps).'
304
743
  } elseif (Get-Command npm -ErrorAction SilentlyContinue) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.10.0",
3
+ "version": "0.10.3",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -27,5 +27,8 @@
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "archgraph-argo": "^0.10.2"
30
33
  }
31
34
  }