@uniqbit/mate-core 0.15.5 → 0.16.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.
- package/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +6 -4
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +47 -0
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -93,13 +93,38 @@ export interface ReportTextSection extends ReportSectionBase {
|
|
|
93
93
|
content: string;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
export interface ReportDiagramSectionBase extends ReportSectionBase {
|
|
97
|
+
type: "diagram";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ReportMermaidDiagramSection extends ReportDiagramSectionBase {
|
|
101
|
+
mermaid: string;
|
|
102
|
+
text?: never;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ReportTextDiagramSection extends ReportDiagramSectionBase {
|
|
106
|
+
text: string;
|
|
107
|
+
mermaid?: never;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Exactly one payload: `mermaid` source drawn by the runtime, or `text` kept as a monospace block. */
|
|
111
|
+
export type ReportDiagramSection = ReportMermaidDiagramSection | ReportTextDiagramSection;
|
|
112
|
+
|
|
113
|
+
export interface ReportDiffSection extends ReportSectionBase {
|
|
114
|
+
type: "diff";
|
|
115
|
+
/** Unified diff as produced by `git diff`. */
|
|
116
|
+
patch: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
export type ReportSection =
|
|
97
120
|
| ReportMetadataSection
|
|
98
121
|
| ReportMetricsSection
|
|
99
122
|
| ReportKeyValueSection
|
|
100
123
|
| ReportTableSection
|
|
101
124
|
| ReportStatusesSection
|
|
102
|
-
| ReportTextSection
|
|
125
|
+
| ReportTextSection
|
|
126
|
+
| ReportDiagramSection
|
|
127
|
+
| ReportDiffSection;
|
|
103
128
|
|
|
104
129
|
export interface ReportDocument {
|
|
105
130
|
version: typeof REPORT_DOCUMENT_VERSION;
|
|
@@ -1,35 +1,132 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
|
+
import {
|
|
5
|
+
CompanionResolver,
|
|
6
|
+
type CompanionMatch,
|
|
7
|
+
} from "../../../lib/orchestrator/companion-resolver";
|
|
2
8
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
9
|
+
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repo-local-registry";
|
|
10
|
+
import { projectWorkingRepositoryBestEffort } from "../../../lib/orchestrator/working-repo-projection";
|
|
11
|
+
import { resolveProjection } from "../../../runtime/projection";
|
|
3
12
|
import { selectCompanion } from "../../companion-selector";
|
|
4
13
|
|
|
5
14
|
export const launchAmbiguityDeps = {
|
|
6
15
|
resolveCompanionMatches: async (cwd: string) =>
|
|
7
16
|
(await new CompanionResolver(new GlobalConfigStore()).resolveWithDiagnostics(cwd))
|
|
8
17
|
.ambiguousMatches,
|
|
18
|
+
/**
|
|
19
|
+
* The resolver reports `ambiguousMatches` as `[]` for an unambiguous link, so
|
|
20
|
+
* the Linked Companions are that list when populated and the single match
|
|
21
|
+
* otherwise.
|
|
22
|
+
*/
|
|
23
|
+
resolveLinkedCompanions: async (cwd: string): Promise<CompanionMatch[]> => {
|
|
24
|
+
const { match, ambiguousMatches } = await new CompanionResolver(
|
|
25
|
+
new GlobalConfigStore(),
|
|
26
|
+
).resolveWithDiagnostics(cwd);
|
|
27
|
+
if (ambiguousMatches.length > 0) return ambiguousMatches;
|
|
28
|
+
return match ? [match] : [];
|
|
29
|
+
},
|
|
9
30
|
selectCompanion,
|
|
31
|
+
findRepoLocalLinkedRepository,
|
|
32
|
+
resolveProjection,
|
|
33
|
+
projectWorkingRepository: projectWorkingRepositoryBestEffort,
|
|
10
34
|
};
|
|
11
35
|
|
|
36
|
+
export interface CompanionSelectionOptions {
|
|
37
|
+
/** Ignore both recorded answers — the launch environment and the projection. */
|
|
38
|
+
reselect?: boolean;
|
|
39
|
+
/** Ignore only the projected answer, so an ambiguous repository is asked again. */
|
|
40
|
+
ignoreProjection?: boolean;
|
|
41
|
+
/** Outranks the environment, the projection, and the picker. */
|
|
42
|
+
companion?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pin(match: Pick<CompanionMatch, "companionPath" | "repositoryId">): void {
|
|
46
|
+
process.env.MATE_ARTIFACT_PATH = match.companionPath;
|
|
47
|
+
process.env.MATE_REPO_ID = match.repositoryId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function reportLinkedCompanions(message: string, matches: CompanionMatch[]): void {
|
|
51
|
+
process.stderr.write(message);
|
|
52
|
+
for (const match of matches) {
|
|
53
|
+
process.stderr.write(` - ${match.companionPath}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rank 1. Validated against the Linked Companions so a wrap can never record a
|
|
59
|
+
* companion that does not link the Working Repository.
|
|
60
|
+
*/
|
|
61
|
+
async function pinExplicitCompanion(cwd: string, companion: string): Promise<boolean> {
|
|
62
|
+
const resolved = path.resolve(companion);
|
|
63
|
+
const linked = await launchAmbiguityDeps.resolveLinkedCompanions(cwd);
|
|
64
|
+
const chosen = linked.find((match) => path.resolve(match.companionPath) === resolved);
|
|
65
|
+
if (!chosen) {
|
|
66
|
+
reportLinkedCompanions(
|
|
67
|
+
`${FRAMEWORK_NAME}: ${resolved} does not link this working repo; linked companions are:\n`,
|
|
68
|
+
linked,
|
|
69
|
+
);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pin(chosen);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Rank 3. Only reached when the link is ambiguous, so `matches` is the full set
|
|
79
|
+
* of Linked Companions — a projected companion absent from it no longer links
|
|
80
|
+
* the repository and is ignored rather than pinning a dead path.
|
|
81
|
+
*/
|
|
82
|
+
function projectedCompanion(cwd: string, matches: CompanionMatch[]): CompanionMatch | null {
|
|
83
|
+
const projected = launchAmbiguityDeps.resolveProjection(cwd);
|
|
84
|
+
if (!projected) return null;
|
|
85
|
+
|
|
86
|
+
const resolved = path.resolve(projected.companionPath);
|
|
87
|
+
return matches.find((match) => path.resolve(match.companionPath) === resolved) ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Best-effort: a read-only Working Repository warns and the command proceeds. */
|
|
91
|
+
async function recordCompanion(cwd: string, chosen: CompanionMatch): Promise<void> {
|
|
92
|
+
const repository = await launchAmbiguityDeps.findRepoLocalLinkedRepository(cwd);
|
|
93
|
+
if (!repository) return;
|
|
94
|
+
|
|
95
|
+
await launchAmbiguityDeps.projectWorkingRepository(chosen.companionPath, repository);
|
|
96
|
+
}
|
|
97
|
+
|
|
12
98
|
/**
|
|
13
99
|
* Pins a companion selection into env vars when the current working repo is
|
|
14
100
|
* linked from multiple companions. Commands that should respect an already
|
|
15
|
-
* selected companion can call this before any context resolution.
|
|
101
|
+
* selected companion can call this before any context resolution. Resolution
|
|
102
|
+
* order: explicit companion, launch environment, Projection Root, picker — the
|
|
103
|
+
* environment above the projection, so no projection can degrade a session Mate
|
|
104
|
+
* configured. A picked companion is recorded at the Projection Root, so the
|
|
105
|
+
* question is asked once rather than once per command.
|
|
16
106
|
*/
|
|
17
107
|
export async function ensureUnambiguousCompanion(
|
|
18
108
|
cwd: string = process.cwd(),
|
|
19
|
-
options:
|
|
109
|
+
options: CompanionSelectionOptions = {},
|
|
20
110
|
): Promise<boolean> {
|
|
111
|
+
if (options.companion) return pinExplicitCompanion(cwd, options.companion);
|
|
21
112
|
if (process.env.MATE_ARTIFACT_PATH && !options.reselect) return true;
|
|
22
113
|
|
|
23
114
|
const ambiguousMatches = await launchAmbiguityDeps.resolveCompanionMatches(cwd);
|
|
24
115
|
if (ambiguousMatches.length <= 1) return true;
|
|
25
116
|
|
|
117
|
+
if (!options.reselect && !options.ignoreProjection) {
|
|
118
|
+
const projected = projectedCompanion(cwd, ambiguousMatches);
|
|
119
|
+
if (projected) {
|
|
120
|
+
pin(projected);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
26
125
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
27
|
-
|
|
28
|
-
|
|
126
|
+
reportLinkedCompanions(
|
|
127
|
+
`${FRAMEWORK_NAME}: this working repo is linked from multiple companions; re-run in a TTY to choose one, run \`${FRAMEWORK_NAME} wrap --companion <path>\`, or set MATE_ARTIFACT_PATH.\n`,
|
|
128
|
+
ambiguousMatches,
|
|
29
129
|
);
|
|
30
|
-
for (const match of ambiguousMatches) {
|
|
31
|
-
process.stderr.write(` - ${match.companionPath}\n`);
|
|
32
|
-
}
|
|
33
130
|
return false;
|
|
34
131
|
}
|
|
35
132
|
|
|
@@ -39,7 +136,7 @@ export async function ensureUnambiguousCompanion(
|
|
|
39
136
|
return false;
|
|
40
137
|
}
|
|
41
138
|
|
|
42
|
-
|
|
43
|
-
|
|
139
|
+
pin(chosen);
|
|
140
|
+
await recordCompanion(cwd, chosen);
|
|
44
141
|
return true;
|
|
45
142
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inline Area binding a requirement carries in a spec body. Anchored to the
|
|
8
|
+
* start of its own line, which is where the marker is written: a spec that
|
|
9
|
+
* merely mentions the marker inside prose or a code span binds no Area by
|
|
10
|
+
* talking about one.
|
|
11
|
+
*/
|
|
12
|
+
const INLINE_AREA = /^\*\*Area:\*\*[ \t]*`([^`\n]+)`/gm;
|
|
13
|
+
|
|
14
|
+
function frontmatterAreas(content: string): string[] {
|
|
15
|
+
if (!content.startsWith("---")) return [];
|
|
16
|
+
const end = content.indexOf("\n---", 3);
|
|
17
|
+
if (end < 0) return [];
|
|
18
|
+
let parsed: unknown;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parse(content.slice(3, end));
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
25
|
+
|
|
26
|
+
const document = parsed as { areas?: unknown; scopes?: unknown };
|
|
27
|
+
const areas = Array.isArray(document.areas)
|
|
28
|
+
? document.areas.filter((entry): entry is string => typeof entry === "string")
|
|
29
|
+
: [];
|
|
30
|
+
const scoped = Array.isArray(document.scopes)
|
|
31
|
+
? document.scopes
|
|
32
|
+
.map((entry) =>
|
|
33
|
+
entry && typeof entry === "object" ? (entry as { area?: unknown }).area : undefined,
|
|
34
|
+
)
|
|
35
|
+
.filter((area): area is string => typeof area === "string")
|
|
36
|
+
: [];
|
|
37
|
+
return [...areas, ...scoped];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Areas one spec binds. No OpenSpec JSON command reports them in bulk — a
|
|
42
|
+
* per-spec `show --json` would cost one process per spec — so the resolved
|
|
43
|
+
* spec document is read directly: frontmatter `areas`/`scopes` unioned with the
|
|
44
|
+
* inline `**Area:**` bindings its requirements carry.
|
|
45
|
+
*/
|
|
46
|
+
export async function readSpecAreas(
|
|
47
|
+
specsRoot: string,
|
|
48
|
+
specId: string,
|
|
49
|
+
readFile: (filePath: string) => Promise<string> = (filePath) => fs.readFile(filePath, "utf8"),
|
|
50
|
+
): Promise<string[]> {
|
|
51
|
+
let content: string;
|
|
52
|
+
try {
|
|
53
|
+
content = await readFile(path.join(specsRoot, specId, "spec.md"));
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const areas = new Set(
|
|
59
|
+
frontmatterAreas(content)
|
|
60
|
+
.map((area) => area.trim())
|
|
61
|
+
.filter(Boolean),
|
|
62
|
+
);
|
|
63
|
+
for (const match of content.matchAll(INLINE_AREA)) {
|
|
64
|
+
const area = match[1]?.trim();
|
|
65
|
+
if (area) areas.add(area);
|
|
66
|
+
}
|
|
67
|
+
return [...areas].toSorted((a, b) => a.localeCompare(b));
|
|
68
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
|
+
import { openReportInBrowser } from "../report/delivery";
|
|
3
|
+
import { serveUntilInterrupted, startStudioServer } from "./server";
|
|
4
|
+
|
|
5
|
+
export interface StudioCommandDeps {
|
|
6
|
+
startStudioServer?: typeof startStudioServer;
|
|
7
|
+
serveUntilInterrupted?: typeof serveUntilInterrupted;
|
|
8
|
+
openInBrowser?: (url: string) => Promise<void>;
|
|
9
|
+
log?: (message: string) => void;
|
|
10
|
+
warn?: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @command mate studio
|
|
15
|
+
* @description Serves a local, read-only page over the Companion Repositories
|
|
16
|
+
* registered on this machine — the workflow drawn from each companion's
|
|
17
|
+
* resolved schema with its OpenSpec state on it — and opens the platform
|
|
18
|
+
* browser at it. Runs in the foreground on an operating-system-assigned
|
|
19
|
+
* loopback port and dies with the process: no daemon, no stop command, and no
|
|
20
|
+
* state written anywhere.
|
|
21
|
+
* @remarks Takes no arguments and resolves no Repository Link, so it runs from
|
|
22
|
+
* any directory.
|
|
23
|
+
*/
|
|
24
|
+
export async function runStudioCommand(
|
|
25
|
+
argv: string[] = [],
|
|
26
|
+
deps: StudioCommandDeps = {},
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const log = deps.log ?? ((message: string) => process.stdout.write(`${message}\n`));
|
|
29
|
+
const warn = deps.warn ?? ((message: string) => process.stderr.write(`${message}\n`));
|
|
30
|
+
|
|
31
|
+
if (argv.length > 0) {
|
|
32
|
+
warn(`${FRAMEWORK_NAME}: \`studio\` takes no arguments; unrecognized: ${argv.join(" ")}`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const start = deps.startStudioServer ?? startStudioServer;
|
|
38
|
+
const serve = deps.serveUntilInterrupted ?? serveUntilInterrupted;
|
|
39
|
+
const open = deps.openInBrowser ?? openReportInBrowser;
|
|
40
|
+
|
|
41
|
+
let server;
|
|
42
|
+
try {
|
|
43
|
+
server = start();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
warn(
|
|
46
|
+
`${FRAMEWORK_NAME}: studio could not bind a port: ${
|
|
47
|
+
error instanceof Error ? error.message : String(error)
|
|
48
|
+
}`,
|
|
49
|
+
);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log(server.url);
|
|
55
|
+
log("Press Ctrl+C to stop.");
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await open(server.url);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
warn(
|
|
61
|
+
`${FRAMEWORK_NAME}: studio could not open a browser: ${
|
|
62
|
+
error instanceof Error ? error.message : String(error)
|
|
63
|
+
}`,
|
|
64
|
+
);
|
|
65
|
+
warn(`${FRAMEWORK_NAME}: open ${server.url} manually; the server keeps serving.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await serve(server);
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectWorkspaceInventory,
|
|
3
|
+
type CompanionInventoryHealth,
|
|
4
|
+
type PairingInventoryHealth,
|
|
5
|
+
} from "../../../lib/orchestrator/workspace-inventory";
|
|
6
|
+
|
|
7
|
+
export interface StudioInventoryPairing {
|
|
8
|
+
repositoryId: string;
|
|
9
|
+
repositoryPath: string;
|
|
10
|
+
health: PairingInventoryHealth;
|
|
11
|
+
ambiguous: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StudioInventoryCompanion {
|
|
15
|
+
path: string;
|
|
16
|
+
health: CompanionInventoryHealth;
|
|
17
|
+
diagnostic?: string;
|
|
18
|
+
pairings: StudioInventoryPairing[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StudioInventory {
|
|
22
|
+
companions: StudioInventoryCompanion[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StudioInventoryDeps {
|
|
26
|
+
collectWorkspaceInventory?: typeof collectWorkspaceInventory;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Companion-first projection of the machine-wide workspace inventory. Reads
|
|
31
|
+
* only the aggregate the workspace surface already resolves — health,
|
|
32
|
+
* ambiguity, and duplicate collapsing stay owned there.
|
|
33
|
+
*/
|
|
34
|
+
export async function collectStudioInventory(
|
|
35
|
+
deps: StudioInventoryDeps = {},
|
|
36
|
+
): Promise<StudioInventory> {
|
|
37
|
+
const collect = deps.collectWorkspaceInventory ?? collectWorkspaceInventory;
|
|
38
|
+
const inventory = await collect();
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
companions: inventory.companions.map((companion) => ({
|
|
42
|
+
path: companion.path,
|
|
43
|
+
health: companion.health,
|
|
44
|
+
...(companion.diagnostic ? { diagnostic: companion.diagnostic } : {}),
|
|
45
|
+
pairings: inventory.pairings
|
|
46
|
+
.filter((pairing) => pairing.companionPath === companion.path)
|
|
47
|
+
.map((pairing) => ({
|
|
48
|
+
repositoryId: pairing.repository.id,
|
|
49
|
+
repositoryPath: pairing.repository.path,
|
|
50
|
+
health: pairing.health,
|
|
51
|
+
ambiguous: pairing.ambiguous,
|
|
52
|
+
})),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface StudioSkillInventory {
|
|
6
|
+
claude: string[];
|
|
7
|
+
opencode: string[];
|
|
8
|
+
agents: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SKILL_DIRS = {
|
|
12
|
+
claude: ".claude",
|
|
13
|
+
opencode: ".opencode",
|
|
14
|
+
agents: ".agents",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
async function skillNamesAt(companionPath: string, runtimeDir: string): Promise<string[]> {
|
|
18
|
+
let entries: Dirent[];
|
|
19
|
+
try {
|
|
20
|
+
entries = await fs.readdir(path.join(companionPath, runtimeDir, "skills"), {
|
|
21
|
+
withFileTypes: true,
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return entries
|
|
28
|
+
.filter((entry) => entry.isDirectory())
|
|
29
|
+
.map((entry) => entry.name)
|
|
30
|
+
.toSorted();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reports every skill directory in each Agent Runtime and the shared Agents tree.
|
|
35
|
+
*/
|
|
36
|
+
export async function collectSkillInventory(companionPath: string): Promise<StudioSkillInventory> {
|
|
37
|
+
const [claude, opencode, agents] = await Promise.all([
|
|
38
|
+
skillNamesAt(companionPath, SKILL_DIRS.claude),
|
|
39
|
+
skillNamesAt(companionPath, SKILL_DIRS.opencode),
|
|
40
|
+
skillNamesAt(companionPath, SKILL_DIRS.agents),
|
|
41
|
+
]);
|
|
42
|
+
return { claude, opencode, agents };
|
|
43
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getWrapperBinPath } from "../../../lib/package-paths";
|
|
5
|
+
|
|
6
|
+
export interface OpenSpecFailure {
|
|
7
|
+
command: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type OpenSpecResult<T> = { ok: true; value: T } | { ok: false; failure: OpenSpecFailure };
|
|
12
|
+
|
|
13
|
+
export interface OpenSpecRunOutput {
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
code: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OpenSpecCliDeps {
|
|
20
|
+
wrapperPath?: () => string;
|
|
21
|
+
run?: (bin: string, args: string[], companionPath: string) => Promise<OpenSpecRunOutput>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The Capability whose CLI produces the workflow a companion's changes move through. */
|
|
25
|
+
export const WORKFLOW_CAPABILITY_ID = "openspec";
|
|
26
|
+
|
|
27
|
+
/** Upper bound on one collection call; a hung wrapper must not hold a request open. */
|
|
28
|
+
const OPENSPEC_TIMEOUT_MS = 60_000;
|
|
29
|
+
|
|
30
|
+
export 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
|
+
function firstJsonValue(stdout: string): unknown {
|
|
68
|
+
const start = stdout.search(/[[{]/);
|
|
69
|
+
if (start < 0) return undefined;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(stdout.slice(start));
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function reasonFor(output: OpenSpecRunOutput): string {
|
|
78
|
+
const detail = (output.stderr.trim() || output.stdout.trim()).split("\n").slice(-3).join("; ");
|
|
79
|
+
const status = `exit code ${output.code ?? "unknown"}`;
|
|
80
|
+
return detail ? `${status}: ${detail}` : status;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
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.
|
|
87
|
+
*/
|
|
88
|
+
export async function readOpenSpecJson<T>(
|
|
89
|
+
companionPath: string,
|
|
90
|
+
args: string[],
|
|
91
|
+
deps: OpenSpecCliDeps = {},
|
|
92
|
+
): Promise<OpenSpecResult<T>> {
|
|
93
|
+
const bin = (deps.wrapperPath ?? openSpecWrapperPath)();
|
|
94
|
+
const command = `openspec ${args.join(" ")}`;
|
|
95
|
+
const run = deps.run ?? runOpenSpecWrapper;
|
|
96
|
+
|
|
97
|
+
let output: OpenSpecRunOutput;
|
|
98
|
+
try {
|
|
99
|
+
output = await run(bin, args, companionPath);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
failure: { command, reason: error instanceof Error ? error.message : String(error) },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `validate` exits non-zero when it finds issues, and its JSON report is
|
|
108
|
+
// exactly what Studio wants in that case — parseable stdout wins over status.
|
|
109
|
+
const parsed = firstJsonValue(output.stdout);
|
|
110
|
+
if (parsed !== undefined) return { ok: true, value: parsed as T };
|
|
111
|
+
|
|
112
|
+
if (output.code !== 0) return { ok: false, failure: { command, reason: reasonFor(output) } };
|
|
113
|
+
return { ok: false, failure: { command, reason: `${command} did not return JSON` } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface OpenSpecChangeListEntry {
|
|
117
|
+
name: string;
|
|
118
|
+
completedTasks?: number;
|
|
119
|
+
totalTasks?: number;
|
|
120
|
+
status?: string;
|
|
121
|
+
lastModified?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface OpenSpecChangeList {
|
|
125
|
+
changes?: OpenSpecChangeListEntry[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface OpenSpecSpecListEntry {
|
|
129
|
+
id: string;
|
|
130
|
+
requirementCount?: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface OpenSpecSpecList {
|
|
134
|
+
specs?: OpenSpecSpecListEntry[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface OpenSpecArtifactStatus {
|
|
138
|
+
id: string;
|
|
139
|
+
status?: string;
|
|
140
|
+
outputPath?: string;
|
|
141
|
+
requires?: string[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface OpenSpecChangeStatusEntry {
|
|
145
|
+
changeName: string;
|
|
146
|
+
schemaName?: string;
|
|
147
|
+
isComplete?: boolean;
|
|
148
|
+
isPlanningComplete?: boolean;
|
|
149
|
+
planningHome?: { root?: string; defaultSchema?: string };
|
|
150
|
+
artifacts?: OpenSpecArtifactStatus[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface OpenSpecAllChangeStatus {
|
|
154
|
+
changes?: OpenSpecChangeStatusEntry[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface OpenSpecValidationItem {
|
|
158
|
+
id: string;
|
|
159
|
+
type?: string;
|
|
160
|
+
valid?: boolean;
|
|
161
|
+
issues?: { level?: string; path?: string; message?: string }[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface OpenSpecValidationReport {
|
|
165
|
+
items?: OpenSpecValidationItem[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function listChanges(
|
|
169
|
+
companionPath: string,
|
|
170
|
+
deps: OpenSpecCliDeps = {},
|
|
171
|
+
): Promise<OpenSpecResult<OpenSpecChangeList>> {
|
|
172
|
+
return readOpenSpecJson(companionPath, ["list", "--json"], deps);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function listSpecs(
|
|
176
|
+
companionPath: string,
|
|
177
|
+
deps: OpenSpecCliDeps = {},
|
|
178
|
+
): Promise<OpenSpecResult<OpenSpecSpecList>> {
|
|
179
|
+
return readOpenSpecJson(companionPath, ["list", "--specs", "--json"], deps);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function readAllChangeStatus(
|
|
183
|
+
companionPath: string,
|
|
184
|
+
deps: OpenSpecCliDeps = {},
|
|
185
|
+
): Promise<OpenSpecResult<OpenSpecAllChangeStatus>> {
|
|
186
|
+
return readOpenSpecJson(companionPath, ["status", "--all", "--json"], deps);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `validate --json` without a selector prints help and exits non-zero, so the
|
|
191
|
+
* whole-root report is the `--all` form.
|
|
192
|
+
*/
|
|
193
|
+
export function validateAll(
|
|
194
|
+
companionPath: string,
|
|
195
|
+
deps: OpenSpecCliDeps = {},
|
|
196
|
+
): Promise<OpenSpecResult<OpenSpecValidationReport>> {
|
|
197
|
+
return readOpenSpecJson(companionPath, ["validate", "--all", "--json"], deps);
|
|
198
|
+
}
|