@uniqbit/mate-core 0.18.5 → 1.0.0-canary.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.
Files changed (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -51,10 +51,16 @@ export interface ReportMetric {
51
51
  detail?: string;
52
52
  }
53
53
 
54
+ export const REPORT_STATUS_TONES = ["ok", "warn", "error", "info", "neutral"] as const;
55
+
56
+ export type ReportStatusTone = (typeof REPORT_STATUS_TONES)[number];
57
+
54
58
  export interface ReportStatus {
55
59
  label: string;
56
60
  status: string;
57
61
  detail?: string;
62
+ /** Presentation tone; inferred from `status` when absent. */
63
+ tone?: ReportStatusTone;
58
64
  }
59
65
 
60
66
  export interface ReportSectionBase {
@@ -88,13 +94,21 @@ export interface ReportStatusesSection extends ReportSectionBase {
88
94
  items: ReportStatus[];
89
95
  }
90
96
 
97
+ export const REPORT_TEXT_FORMATS = ["plain", "markdown"] as const;
98
+
99
+ export type ReportTextFormat = (typeof REPORT_TEXT_FORMATS)[number];
100
+
91
101
  export interface ReportTextSection extends ReportSectionBase {
92
102
  type: "text";
93
103
  content: string;
104
+ /** `plain` (default) keeps the text verbatim; `markdown` renders the safe subset. */
105
+ format?: ReportTextFormat;
94
106
  }
95
107
 
96
108
  export interface ReportDiagramSectionBase extends ReportSectionBase {
97
109
  type: "diagram";
110
+ /** Markdown note rendered beneath the visual. */
111
+ caption?: string;
98
112
  }
99
113
 
100
114
  export interface ReportMermaidDiagramSection extends ReportDiagramSectionBase {
@@ -114,6 +128,8 @@ export interface ReportDiffSection extends ReportSectionBase {
114
128
  type: "diff";
115
129
  /** Unified diff as produced by `git diff`. */
116
130
  patch: string;
131
+ /** Markdown note rendered beneath the visual. */
132
+ caption?: string;
117
133
  }
118
134
 
119
135
  export type ReportSection =
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../lib/orchestrator/setup-preflight";
10
10
  import {
11
11
  getSetupSelectionsFromConfig,
12
- applyOpenSpecSchemaSelection,
12
+ keepCapabilitySettings,
13
13
  } from "../../lib/orchestrator/setup-compatibilities";
14
14
  import { confirm } from "../confirm";
15
15
  import { selectCompanion } from "../companion-selector";
@@ -19,7 +19,6 @@ import {
19
19
  parseCapabilities,
20
20
  parseGitMode,
21
21
  parseFlags,
22
- parseOpenSpecSchema,
23
22
  parsePackageManagers,
24
23
  } from "../parse-flags";
25
24
 
@@ -59,11 +58,10 @@ export async function runSetupCommand(argv: string[]): Promise<void> {
59
58
  * via `setup.execute` and prints a summary table of allowed agents, package managers,
60
59
  * and capabilities.
61
60
  * @flags
62
- * - `--agents <list>` — parsed via `parseAllowedAgents`.
63
- * - `--package-managers <list>` — parsed via `parsePackageManagers`.
64
- * - `--capabilities <list>` — parsed via `parseCapabilities`.
65
- * - `--openspec-schema <value>` — parsed via `parseOpenSpecSchema`.
66
- * - `--git-mode <value>` — parsed via `parseGitMode`.
61
+ * - `--allowed-agent <agent>` — repeatable; parsed via `parseAllowedAgents`.
62
+ * - `--package-manager <name>` — repeatable; parsed via `parsePackageManagers`.
63
+ * - `--capability <name>` — repeatable; parsed via `parseCapabilities`.
64
+ * - `--git-mode <auto|default>` — parsed via `parseGitMode`.
67
65
  * @remarks When any flag above is present, the whole selection is treated as
68
66
  * explicit and the interactive picker is skipped.
69
67
  */
@@ -142,13 +140,11 @@ async function runSetupFlow(
142
140
  const flagAllowedAgents = parseAllowedAgents(flags);
143
141
  const flagPackageManagers = parsePackageManagers(flags);
144
142
  const flagCapabilities = parseCapabilities(flags);
145
- const flagOpenSpecSchema = parseOpenSpecSchema(flags);
146
143
  const flagGitMode = parseGitMode(flags);
147
144
  const hasExplicitSelections =
148
145
  flagAllowedAgents !== undefined ||
149
146
  flagPackageManagers !== undefined ||
150
147
  flagCapabilities !== undefined ||
151
- flagOpenSpecSchema !== undefined ||
152
148
  flagGitMode !== undefined;
153
149
 
154
150
  const currentSelections =
@@ -164,12 +160,8 @@ async function runSetupFlow(
164
160
  ? {
165
161
  allowedAgents: flagAllowedAgents ?? currentSelections.allowedAgents,
166
162
  packageManagers: flagPackageManagers ?? currentSelections.packageManagers,
167
- capabilities: applyOpenSpecSchemaSelection(
168
- flagCapabilities ?? currentSelections.capabilities,
169
- flagOpenSpecSchema,
170
- ),
163
+ capabilities: flagCapabilities ?? currentSelections.capabilities,
171
164
  git: flagGitMode ?? currentSelections.selectedGitMode,
172
- selectedOpenSpecSchema: flagOpenSpecSchema ?? currentSelections.selectedOpenSpecSchema,
173
165
  selectedGitMode: flagGitMode ?? currentSelections.selectedGitMode,
174
166
  }
175
167
  : await selectCompatibilities({ initialSelections: currentSelections });
@@ -183,7 +175,7 @@ async function runSetupFlow(
183
175
  {
184
176
  allowedAgents: selections.allowedAgents,
185
177
  packageManagers: selections.packageManagers,
186
- capabilities: selections.capabilities,
178
+ capabilities: keepCapabilitySettings(selections.capabilities, currentSelections.capabilities),
187
179
  git: selections.selectedGitMode,
188
180
  },
189
181
  { cwd: targetCwd },
@@ -0,0 +1,16 @@
1
+ /** Where Studio serves its own client script: one fixed name, never mapped to a path. */
2
+ export const STUDIO_CLIENT_ROUTE = "/studio/client.js";
3
+
4
+ /** `null` for any name but the fixed one, so a request never maps to a file. */
5
+ export async function studioClientAsset(pathname: string): Promise<Response | null> {
6
+ if (pathname !== STUDIO_CLIENT_ROUTE) return null;
7
+ // Loaded on request, so the CLI's startup never reads the built script.
8
+ const { STUDIO_CLIENT_BUNDLE } = await import("./views/studio-client.generated");
9
+ return new Response(STUDIO_CLIENT_BUNDLE, {
10
+ headers: {
11
+ "content-type": "text/javascript; charset=utf-8",
12
+ "cache-control": "no-cache",
13
+ "x-content-type-options": "nosniff",
14
+ },
15
+ });
16
+ }
@@ -1,7 +1,4 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
-
4
- import { getWrapperBinPath } from "../../../lib/package-paths";
1
+ import { runOpenSpec, type OpenSpecRun, type RunOpenSpec } from "../../../lib/openspec-cli";
5
2
 
6
3
  export interface OpenSpecFailure {
7
4
  command: string;
@@ -10,60 +7,16 @@ export interface OpenSpecFailure {
10
7
 
11
8
  export type OpenSpecResult<T> = { ok: true; value: T } | { ok: false; failure: OpenSpecFailure };
12
9
 
13
- export interface OpenSpecRunOutput {
14
- stdout: string;
15
- stderr: string;
16
- code: number | null;
17
- }
18
-
19
10
  export interface OpenSpecCliDeps {
20
- wrapperPath?: () => string;
21
- run?: (bin: string, args: string[], companionPath: string) => Promise<OpenSpecRunOutput>;
11
+ run?: RunOpenSpec;
22
12
  }
23
13
 
24
14
  /** The Capability whose CLI produces the workflow a companion's changes move through. */
25
15
  export const WORKFLOW_CAPABILITY_ID = "openspec";
26
16
 
27
- /** Upper bound on one collection call; a hung wrapper must not hold a request open. */
17
+ /** Upper bound on one collection call; a hung CLI must not hold a request open. */
28
18
  const OPENSPEC_TIMEOUT_MS = 60_000;
29
19
 
30
- function openSpecWrapperPath(): string {
31
- return path.join(getWrapperBinPath(), "openspec");
32
- }
33
-
34
- /**
35
- * The wrapper resolves its OpenSpec root from `MATE_ARTIFACT_PATH`, so the
36
- * companion is selected by environment rather than by cwd — the studio server
37
- * has no Repository Link to walk up from.
38
- */
39
- function runOpenSpecWrapper(
40
- bin: string,
41
- args: string[],
42
- companionPath: string,
43
- ): Promise<OpenSpecRunOutput> {
44
- return new Promise((resolve, reject) => {
45
- const child = spawn(bin, args, {
46
- cwd: companionPath,
47
- env: { ...process.env, MATE_ARTIFACT_PATH: companionPath },
48
- stdio: ["ignore", "pipe", "pipe"],
49
- timeout: OPENSPEC_TIMEOUT_MS,
50
- });
51
-
52
- let stdout = "";
53
- let stderr = "";
54
- child.stdout?.setEncoding("utf8");
55
- child.stderr?.setEncoding("utf8");
56
- child.stdout?.on("data", (chunk: string) => {
57
- stdout += chunk;
58
- });
59
- child.stderr?.on("data", (chunk: string) => {
60
- stderr += chunk;
61
- });
62
- child.once("error", reject);
63
- child.once("close", (code) => resolve({ stdout, stderr, code }));
64
- });
65
- }
66
-
67
20
  function firstJsonValue(stdout: string): unknown {
68
21
  const start = stdout.search(/[[{]/);
69
22
  if (start < 0) return undefined;
@@ -74,29 +27,28 @@ function firstJsonValue(stdout: string): unknown {
74
27
  }
75
28
  }
76
29
 
77
- function reasonFor(output: OpenSpecRunOutput): string {
30
+ function reasonFor(output: OpenSpecRun): string {
78
31
  const detail = (output.stderr.trim() || output.stdout.trim()).split("\n").slice(-3).join("; ");
79
32
  const status = `exit code ${output.code ?? "unknown"}`;
80
33
  return detail ? `${status}: ${detail}` : status;
81
34
  }
82
35
 
83
36
  /**
84
- * Reads one OpenSpec JSON command for a Companion Repository. Never throws:
85
- * every failure — spawn, exit status, unparseable output — becomes a typed
86
- * failure so one unreadable companion cannot take the server down.
37
+ * Reads one OpenSpec JSON command, run in the Companion Repository. Never
38
+ * throws: every failure — spawn, exit status, unparseable output — becomes a
39
+ * typed failure so one unreadable companion cannot take the server down.
87
40
  */
88
41
  export async function readOpenSpecJson<T>(
89
42
  companionPath: string,
90
43
  args: string[],
91
44
  deps: OpenSpecCliDeps = {},
92
45
  ): Promise<OpenSpecResult<T>> {
93
- const bin = (deps.wrapperPath ?? openSpecWrapperPath)();
94
46
  const command = `openspec ${args.join(" ")}`;
95
- const run = deps.run ?? runOpenSpecWrapper;
47
+ const run = deps.run ?? runOpenSpec;
96
48
 
97
- let output: OpenSpecRunOutput;
49
+ let output: OpenSpecRun;
98
50
  try {
99
- output = await run(bin, args, companionPath);
51
+ output = await run(args, { cwd: companionPath, timeout: OPENSPEC_TIMEOUT_MS });
100
52
  } catch (error) {
101
53
  return {
102
54
  ok: false,
@@ -5,6 +5,8 @@ export const STUDIO_HOSTNAME = "127.0.0.1";
5
5
  export const VAULT_DIR_ROUTE = "/api/vault/dir";
6
6
  /** Name-filter matches over the whole listed tree, as an HTML fragment. */
7
7
  export const VAULT_FILTER_ROUTE = "/api/vault/filter";
8
+ /** The open file's breadcrumb and pane as an HTML fragment, for opening a file in place. */
9
+ export const VAULT_PANE_ROUTE = "/api/vault/pane";
8
10
  /** Server-sent listing generations: a newer one marks the displayed tree stale. */
9
11
  export const VAULT_CHANGES_ROUTE = "/api/vault/changes";
10
12
 
@@ -18,6 +18,7 @@ export const VIEW_PARAM = "view";
18
18
  export const REFRESH_PARAM = "refresh";
19
19
  export const FILE_PARAM = "path";
20
20
  export const DIR_PARAM = "dir";
21
+ export const MODE_PARAM = "mode";
21
22
 
22
23
  /** Every URL parameter Studio names; no other module writes one out. */
23
24
  export const STUDIO_PARAMS: readonly string[] = [
@@ -26,6 +27,7 @@ export const STUDIO_PARAMS: readonly string[] = [
26
27
  REFRESH_PARAM,
27
28
  FILE_PARAM,
28
29
  DIR_PARAM,
30
+ MODE_PARAM,
29
31
  ];
30
32
 
31
33
  /** Filled into a prompt when no change is named, so a prompt is never half-written. */
@@ -42,9 +44,14 @@ interface SelectionBase {
42
44
  refresh: boolean;
43
45
  }
44
46
 
47
+ /** An open file is read rendered or edited raw; read is every other value. */
48
+ export type VaultMode = "read" | "edit";
49
+
45
50
  export interface StudioVaultSelection extends SelectionBase {
46
51
  view: "vault";
47
52
  openPath: string | null;
53
+ /** Only meaningful with an open file, and only where the write path is reached. */
54
+ mode: VaultMode;
48
55
  /** A folder of the listed tree; never looked up on disk. */
49
56
  openDir: string | null;
50
57
  }
@@ -109,6 +116,7 @@ function readVault(params: URLSearchParams): StudioVaultSelection {
109
116
  ...readBase(params),
110
117
  view: "vault",
111
118
  openPath: readText(params.get(FILE_PARAM)),
119
+ mode: params.get(MODE_PARAM) === "edit" ? "edit" : "read",
112
120
  openDir: readText(params.get(DIR_PARAM)),
113
121
  };
114
122
  }
@@ -128,7 +136,7 @@ export function parseVaultSelection(url: URL): StudioVaultSelection {
128
136
  /** Drops the vault's target. */
129
137
  export function switchView(selection: StudioSelection, view: StudioView): StudioSelection {
130
138
  const base = { companionDigest: selection.companionDigest, refresh: false };
131
- if (view === "vault") return { ...base, view, openPath: null, openDir: null };
139
+ if (view === "vault") return { ...base, view, openPath: null, mode: "read", openDir: null };
132
140
  return { ...base, view };
133
141
  }
134
142
 
@@ -146,6 +154,7 @@ export function openFile(selection: StudioSelection, path: string): StudioVaultS
146
154
  refresh: false,
147
155
  view: "vault",
148
156
  openPath: path,
157
+ mode: "read",
149
158
  openDir: null,
150
159
  };
151
160
  }
@@ -157,10 +166,16 @@ export function openFolder(selection: StudioSelection, dir: string | null): Stud
157
166
  refresh: false,
158
167
  view: "vault",
159
168
  openPath: null,
169
+ mode: "read",
160
170
  openDir: dir,
161
171
  };
162
172
  }
163
173
 
174
+ /** The open file in the other mode; a selection with no open file is returned as it is. */
175
+ export function withMode(selection: StudioVaultSelection, mode: VaultMode): StudioVaultSelection {
176
+ return selection.openPath ? { ...selection, mode, refresh: false } : selection;
177
+ }
178
+
164
179
  export function refresh(selection: StudioSelection): StudioSelection {
165
180
  return { ...selection, refresh: true };
166
181
  }
@@ -174,6 +189,9 @@ export function toFields(selection: StudioSelection): SelectionField[] {
174
189
  if (selection.view !== "dashboard") fields.push({ name: VIEW_PARAM, value: selection.view });
175
190
  if (selection.view === "vault") {
176
191
  if (selection.openPath) fields.push({ name: FILE_PARAM, value: selection.openPath });
192
+ if (selection.openPath && selection.mode === "edit") {
193
+ fields.push({ name: MODE_PARAM, value: "edit" });
194
+ }
177
195
  if (selection.openDir) fields.push({ name: DIR_PARAM, value: selection.openDir });
178
196
  }
179
197
  if (selection.refresh) fields.push({ name: REFRESH_PARAM, value: "1" });
@@ -18,6 +18,7 @@ import {
18
18
  VAULT_CHANGES_ROUTE,
19
19
  VAULT_DIR_ROUTE,
20
20
  VAULT_FILTER_ROUTE,
21
+ VAULT_PANE_ROUTE,
21
22
  } from "./routes";
22
23
  import {
23
24
  COMPANION_DIGEST_PATTERN,
@@ -37,6 +38,7 @@ import {
37
38
  type TerminalConnection,
38
39
  type TerminalRegistryOptions,
39
40
  } from "./terminal";
41
+ import { studioClientAsset } from "./client-asset";
40
42
  import { terminalAsset } from "./terminal-assets";
41
43
  import {
42
44
  createLaunchResolver,
@@ -143,6 +145,11 @@ async function renderVaultView(page: StudioPage): Promise<string> {
143
145
  return views.renderVaultView(page);
144
146
  }
145
147
 
148
+ async function renderVaultPane(page: StudioPage): Promise<string> {
149
+ const views = await import("./views/document");
150
+ return views.renderVaultPane(page);
151
+ }
152
+
146
153
  async function renderVaultChildren(page: StudioPage): Promise<string | null> {
147
154
  const views = await import("./views/document");
148
155
  return views.renderVaultChildren(page);
@@ -158,12 +165,17 @@ async function collectVaultState(
158
165
  vault: VaultManager,
159
166
  companionPath: string,
160
167
  selection: StudioVaultSelection,
161
- options: { tree: "prefetch" | "await" },
168
+ options: { tree: "prefetch" | "await" | "skip" },
162
169
  ): Promise<StudioVaultPage> {
163
170
  const { vaultState } = await import("./views/vault/state");
164
171
  return vaultState(vault, companionPath, selection, options);
165
172
  }
166
173
 
174
+ /** A pane is never a refresh, so the listing it skips is not asked to be rebuilt. */
175
+ function plainSelection(selection: StudioVaultSelection): StudioVaultSelection {
176
+ return { ...selection, refresh: false };
177
+ }
178
+
167
179
  function html(body: string): Response {
168
180
  return new Response(body, {
169
181
  headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
@@ -435,6 +447,9 @@ export function createStudioFetch(
435
447
  );
436
448
  }
437
449
 
450
+ const clientAsset = await studioClientAsset(url.pathname);
451
+ if (clientAsset) return respond(clientAsset);
452
+
438
453
  if (url.pathname.startsWith("/studio/terminal/") && terminal) {
439
454
  const asset = await terminalAsset(url.pathname);
440
455
  if (asset) return respond(asset);
@@ -527,6 +542,32 @@ export function createStudioFetch(
527
542
  );
528
543
  }
529
544
 
545
+ if (url.pathname === VAULT_PANE_ROUTE) {
546
+ const collected = await inventory();
547
+ const selection = parseVaultSelection(url);
548
+ const companion = resolveCompanion(collected, selection.companionDigest);
549
+ if (!companion) return respond(json({ reason: "no registered companion was selected" }, 400));
550
+ const state = await collectVaultState(vault, companion.path, plainSelection(selection), {
551
+ tree: "skip",
552
+ });
553
+ return respond(
554
+ new Response(
555
+ await renderVaultPane({
556
+ inventory: collected,
557
+ selection,
558
+ companion,
559
+ payload: null,
560
+ error: null,
561
+ collectedAt: null,
562
+ writable,
563
+ vault: state,
564
+ terminal: null,
565
+ }),
566
+ { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } },
567
+ ),
568
+ );
569
+ }
570
+
530
571
  if (url.pathname === VAULT_DIR_ROUTE || url.pathname === VAULT_FILTER_ROUTE) {
531
572
  const selection = parseVaultSelection(url);
532
573
  const selected = await selectedCompanion(selection.companionDigest, inventory);