@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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkingTreeChange } from "../finish/git";
|
|
5
|
+
|
|
6
|
+
/** Whether an archived change still has uncommitted content in the companion working tree. */
|
|
7
|
+
export type CommitState = "committed" | "uncommitted";
|
|
8
|
+
|
|
9
|
+
/** An archive whose delta specs name a canonical spec. Evidence, never proof of authorship. */
|
|
10
|
+
export interface SpecAttribution {
|
|
11
|
+
/** Dated archive directory name, e.g. `2026-09-07-acme`. */
|
|
12
|
+
anchor: string;
|
|
13
|
+
state: CommitState;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** An uncommitted canonical spec no pending change accounts for, with its attribution. */
|
|
17
|
+
export interface UnattributedSpec {
|
|
18
|
+
/** Companion-relative POSIX path of the canonical spec. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Whether the spec is new or already tracked and modified. */
|
|
21
|
+
kind: WorkingTreeChange;
|
|
22
|
+
/** Every archive naming this spec, oldest anchor first; empty when none does. */
|
|
23
|
+
touchedByArchives: SpecAttribution[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UncommittedSpecChange {
|
|
27
|
+
/** Companion-relative POSIX path of the canonical spec. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Whether the spec is new or already tracked and modified. */
|
|
30
|
+
kind: WorkingTreeChange;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ArchiveEntry {
|
|
34
|
+
/** Change name with the archive date prefix stripped. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Dated archive directory name, e.g. `2026-09-07-acme`. */
|
|
37
|
+
anchor: string;
|
|
38
|
+
/** Companion-relative POSIX path of the archive directory. */
|
|
39
|
+
path: string;
|
|
40
|
+
/** Tag `mate artifact publish` creates for this anchor. */
|
|
41
|
+
tag: string;
|
|
42
|
+
/** Uncommitted owned directories that make this change pending. */
|
|
43
|
+
uncommittedPaths: string[];
|
|
44
|
+
/** Uncommitted canonical specs this change's delta specs applied to. */
|
|
45
|
+
uncommittedSpecs: string[];
|
|
46
|
+
/** Uncommitted canonical specs with their working-tree change kind. */
|
|
47
|
+
uncommittedSpecChanges: UncommittedSpecChange[];
|
|
48
|
+
state: CommitState;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Only dated archive directories are publishable; everything else is ignored. */
|
|
52
|
+
const ARCHIVE_ANCHOR_PATTERN = /^\d{4}-\d{2}-\d{2}-.+$/;
|
|
53
|
+
const ARCHIVE_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
|
|
54
|
+
|
|
55
|
+
export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
|
|
56
|
+
export const SPECS_RELATIVE_DIR = "openspec/specs";
|
|
57
|
+
|
|
58
|
+
/** The tag `mate artifact publish` creates, never a recomputed date. */
|
|
59
|
+
export function finishMarker(anchor: string): string {
|
|
60
|
+
return `openspec/${anchor}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Porcelain collapses an untracked directory to `dir/`; compare without that suffix. */
|
|
64
|
+
function withoutTrailingSlash(candidate: string): string {
|
|
65
|
+
return candidate.endsWith("/") ? candidate.slice(0, -1) : candidate;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** True when the reported path is the owned path, sits inside it, or collapsed it. */
|
|
69
|
+
function touches(worktreePath: string, ownedPath: string): boolean {
|
|
70
|
+
const reported = withoutTrailingSlash(worktreePath);
|
|
71
|
+
return (
|
|
72
|
+
reported === ownedPath ||
|
|
73
|
+
reported.startsWith(`${ownedPath}/`) ||
|
|
74
|
+
ownedPath.startsWith(`${reported}/`)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matching(uncommittedPaths: string[], owned: string[]): string[] {
|
|
79
|
+
return [
|
|
80
|
+
...new Set(
|
|
81
|
+
uncommittedPaths.filter((worktreePath) =>
|
|
82
|
+
owned.some((ownedPath) => touches(worktreePath, ownedPath)),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
].toSorted();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function matchingDirectories(uncommittedPaths: string[], owned: string[]): string[] {
|
|
89
|
+
return owned
|
|
90
|
+
.flatMap((ownedPath) =>
|
|
91
|
+
uncommittedPaths.some((worktreePath) => touches(worktreePath, ownedPath))
|
|
92
|
+
? [ownedPath.endsWith("/") ? ownedPath : `${ownedPath}/`]
|
|
93
|
+
: [],
|
|
94
|
+
)
|
|
95
|
+
.toSorted();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function changeKindFor(
|
|
99
|
+
worktreePath: string,
|
|
100
|
+
kinds: Readonly<Record<string, WorkingTreeChange>>,
|
|
101
|
+
): WorkingTreeChange {
|
|
102
|
+
const direct = kinds[worktreePath] ?? kinds[withoutTrailingSlash(worktreePath)];
|
|
103
|
+
if (direct) return direct;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
Object.entries(kinds).find(([candidate]) =>
|
|
107
|
+
touches(worktreePath, withoutTrailingSlash(candidate)),
|
|
108
|
+
)?.[1] ?? "modified"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Canonical specs the change's delta specs applied to. Only directory entry names under
|
|
114
|
+
* the archive's `specs/` tree are read — never any file contents — so archived prose
|
|
115
|
+
* cannot influence discovery.
|
|
116
|
+
*/
|
|
117
|
+
async function canonicalSpecsFor(companionPath: string, anchor: string): Promise<string[]> {
|
|
118
|
+
const collectDeltaSpecFiles = async (directory: string, relative = ""): Promise<string[]> => {
|
|
119
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
120
|
+
const nested = await Promise.all(
|
|
121
|
+
entries.map(async (entry) => {
|
|
122
|
+
const entryRelative = path.posix.join(relative, entry.name);
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
return collectDeltaSpecFiles(path.join(directory, entry.name), entryRelative);
|
|
125
|
+
}
|
|
126
|
+
return entry.isFile() ? [path.posix.join(SPECS_RELATIVE_DIR, entryRelative)] : [];
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
return nested.flat();
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
const specs = await collectDeltaSpecFiles(
|
|
133
|
+
path.join(companionPath, ARCHIVE_RELATIVE_DIR, anchor, "specs"),
|
|
134
|
+
);
|
|
135
|
+
return specs.toSorted();
|
|
136
|
+
} catch {
|
|
137
|
+
// A change without delta specs still owns its archive and active directories.
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Lists every dated archive directory with its commit state. State comes from the paths a
|
|
144
|
+
* change owns outright — its archive directory and the active directory archiving deleted
|
|
145
|
+
* — because canonical specs are shared: a spec today's work modified would otherwise
|
|
146
|
+
* resurrect every long-published change that ever touched it. Owned specs are still
|
|
147
|
+
* reported on a pending entry so the publication scope is visible. Ordering is the stable
|
|
148
|
+
* anchor sort (oldest date first).
|
|
149
|
+
*/
|
|
150
|
+
export async function discoverArchives(
|
|
151
|
+
companionPath: string,
|
|
152
|
+
uncommittedPaths: string[],
|
|
153
|
+
changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
|
|
154
|
+
): Promise<ArchiveEntry[]> {
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = await fs.readdir(path.join(companionPath, "openspec", "changes", "archive"), {
|
|
158
|
+
withFileTypes: true,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const anchors = entries
|
|
165
|
+
.filter((entry) => entry.isDirectory() && ARCHIVE_ANCHOR_PATTERN.test(entry.name))
|
|
166
|
+
.map((entry) => entry.name)
|
|
167
|
+
.toSorted();
|
|
168
|
+
|
|
169
|
+
return Promise.all(
|
|
170
|
+
anchors.map(async (anchor) => {
|
|
171
|
+
const name = anchor.replace(ARCHIVE_DATE_PREFIX, "");
|
|
172
|
+
const ownedPaths = [`${ARCHIVE_RELATIVE_DIR}/${anchor}`, `openspec/changes/${name}`];
|
|
173
|
+
const owned = matching(uncommittedPaths, ownedPaths);
|
|
174
|
+
const ownedDirectories = matchingDirectories(uncommittedPaths, ownedPaths);
|
|
175
|
+
const specs =
|
|
176
|
+
owned.length > 0
|
|
177
|
+
? matching(uncommittedPaths, await canonicalSpecsFor(companionPath, anchor))
|
|
178
|
+
: [];
|
|
179
|
+
const specChanges = specs.map((specPath) => ({
|
|
180
|
+
path: specPath,
|
|
181
|
+
kind: changeKindFor(specPath, changeKinds),
|
|
182
|
+
}));
|
|
183
|
+
return {
|
|
184
|
+
name,
|
|
185
|
+
anchor,
|
|
186
|
+
path: `${ARCHIVE_RELATIVE_DIR}/${anchor}`,
|
|
187
|
+
tag: finishMarker(anchor),
|
|
188
|
+
uncommittedPaths: ownedDirectories,
|
|
189
|
+
uncommittedSpecs: specs,
|
|
190
|
+
uncommittedSpecChanges: specChanges,
|
|
191
|
+
state: (owned.length > 0 ? "uncommitted" : "committed") as CommitState,
|
|
192
|
+
};
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function pendingArchives(archives: ArchiveEntry[]): ArchiveEntry[] {
|
|
198
|
+
return archives.filter((archive) => archive.state === "uncommitted");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Every archive with the canonical specs its delta specs name, in the caller's archive
|
|
203
|
+
* order. Built once per call and only when something is unattributed, so the common
|
|
204
|
+
* `pending` invocation keeps its current I/O.
|
|
205
|
+
*/
|
|
206
|
+
async function attributionIndex(
|
|
207
|
+
companionPath: string,
|
|
208
|
+
archives: ArchiveEntry[],
|
|
209
|
+
): Promise<Array<SpecAttribution & { specs: string[] }>> {
|
|
210
|
+
return Promise.all(
|
|
211
|
+
archives.map(async (archive) => ({
|
|
212
|
+
anchor: archive.anchor,
|
|
213
|
+
state: archive.state,
|
|
214
|
+
specs: await canonicalSpecsFor(companionPath, archive.anchor),
|
|
215
|
+
})),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Uncommitted canonical specs that no pending change accounts for, each attributed to
|
|
221
|
+
* every archive whose delta specs name it. An attributed spec belongs to a change whose
|
|
222
|
+
* archive is already committed and can be resumed by publishing that change by name; an
|
|
223
|
+
* unattributed one belongs to work that is not archived yet. `archives` is the full
|
|
224
|
+
* archive list, not just the pending ones, because attribution's whole point is naming
|
|
225
|
+
* the committed archives pending discovery excludes.
|
|
226
|
+
*/
|
|
227
|
+
export async function unattributedSpecs(
|
|
228
|
+
companionPath: string,
|
|
229
|
+
uncommittedPaths: string[],
|
|
230
|
+
archives: ArchiveEntry[],
|
|
231
|
+
changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
|
|
232
|
+
): Promise<UnattributedSpec[]> {
|
|
233
|
+
const claimed = new Set(pendingArchives(archives).flatMap((entry) => entry.uncommittedSpecs));
|
|
234
|
+
const specPaths = uncommittedPaths
|
|
235
|
+
.filter(
|
|
236
|
+
(worktreePath) => touches(worktreePath, SPECS_RELATIVE_DIR) && !claimed.has(worktreePath),
|
|
237
|
+
)
|
|
238
|
+
.toSorted();
|
|
239
|
+
if (specPaths.length === 0) return [];
|
|
240
|
+
|
|
241
|
+
const index = await attributionIndex(companionPath, archives);
|
|
242
|
+
return specPaths.map((specPath) => ({
|
|
243
|
+
path: specPath,
|
|
244
|
+
kind: changeKindFor(specPath, changeKinds),
|
|
245
|
+
touchedByArchives: index.flatMap(({ anchor, state, specs }) =>
|
|
246
|
+
specs.some((spec) => touches(specPath, spec)) ? [{ anchor, state }] : [],
|
|
247
|
+
),
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { runArtifactPendingCommand } from "./command";
|
|
2
|
+
export type { PendingCommandDeps, PendingResult } from "./command";
|
|
3
|
+
export {
|
|
4
|
+
ARCHIVE_RELATIVE_DIR,
|
|
5
|
+
SPECS_RELATIVE_DIR,
|
|
6
|
+
discoverArchives,
|
|
7
|
+
finishMarker,
|
|
8
|
+
pendingArchives,
|
|
9
|
+
unattributedSpecs,
|
|
10
|
+
} from "./discovery";
|
|
11
|
+
export type {
|
|
12
|
+
ArchiveEntry,
|
|
13
|
+
CommitState,
|
|
14
|
+
SpecAttribution,
|
|
15
|
+
UnattributedSpec,
|
|
16
|
+
UncommittedSpecChange,
|
|
17
|
+
} from "./discovery";
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
|
|
9
9
|
import type { CapabilityConfig } from "../../../lib/orchestrator/types";
|
|
10
10
|
import {
|
|
11
|
+
ensureTokensaveBranchingPosture,
|
|
11
12
|
ensureTokensaveStoreExcluded,
|
|
12
13
|
TOKENSAVE_STORE_DIR,
|
|
13
14
|
} from "../../../tools/setup/capabilities/tokensave";
|
|
@@ -24,6 +25,7 @@ export interface SyncCapDeps {
|
|
|
24
25
|
loadCapabilities?: () => Promise<CapabilityConfig[]>;
|
|
25
26
|
isTokensaveInitialized?: () => Promise<boolean>;
|
|
26
27
|
ensureTokensaveStoreExcluded?: (repoPath: string) => Promise<void>;
|
|
28
|
+
ensureTokensaveBranchingPosture?: (repoPath: string) => Promise<void>;
|
|
27
29
|
runGraphify?: CapRunner;
|
|
28
30
|
runTokensave?: CapRunner;
|
|
29
31
|
onStepStart?: (step: SyncCapStep) => void;
|
|
@@ -122,6 +124,8 @@ export async function runIndexCapCommand(args: string[], deps: SyncCapDeps = {})
|
|
|
122
124
|
const ensureCompanion = deps.ensureUnambiguousCompanion ?? ensureUnambiguousCompanion;
|
|
123
125
|
const isTokensaveInitialized = deps.isTokensaveInitialized ?? defaultIsTokensaveInitialized;
|
|
124
126
|
const ensureStoreExcluded = deps.ensureTokensaveStoreExcluded ?? ensureTokensaveStoreExcluded;
|
|
127
|
+
const ensureBranchingPosture =
|
|
128
|
+
deps.ensureTokensaveBranchingPosture ?? ensureTokensaveBranchingPosture;
|
|
125
129
|
const runGraphify = deps.runGraphify ?? runGraphifyCapCommand;
|
|
126
130
|
const runTokensave = deps.runTokensave ?? runTokensaveCapCommand;
|
|
127
131
|
|
|
@@ -152,18 +156,22 @@ export async function runIndexCapCommand(args: string[], deps: SyncCapDeps = {})
|
|
|
152
156
|
|
|
153
157
|
if (!options.includeGraphify && hasTokensaveCapability(capabilities)) {
|
|
154
158
|
onStepStart("tokensave");
|
|
159
|
+
const repoPath = process.env.MATE_REPO_PATH ?? process.cwd();
|
|
155
160
|
if (!(await isTokensaveInitialized())) {
|
|
156
|
-
await ensureStoreExcluded(
|
|
161
|
+
await ensureStoreExcluded(repoPath);
|
|
157
162
|
const initOk = await runCapStep(() => runTokensave(["init"]));
|
|
158
163
|
if (!initOk) {
|
|
159
164
|
failed = true;
|
|
160
165
|
onStepDone("tokensave", false);
|
|
161
166
|
} else {
|
|
167
|
+
/** The store config only exists once `tokensave init` has created it. */
|
|
168
|
+
await ensureBranchingPosture(repoPath);
|
|
162
169
|
const syncOk = await runCapStep(() => runTokensave(["sync"]));
|
|
163
170
|
if (!syncOk) failed = true;
|
|
164
171
|
onStepDone("tokensave", syncOk);
|
|
165
172
|
}
|
|
166
173
|
} else {
|
|
174
|
+
await ensureBranchingPosture(repoPath);
|
|
167
175
|
const syncOk = await runCapStep(() => runTokensave(["sync"]));
|
|
168
176
|
if (!syncOk) failed = true;
|
|
169
177
|
onStepDone("tokensave", syncOk);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { runGraphifyCapCommand } from "./graphify";
|
|
2
|
-
import { runHeadroomCapCommand } from "./headroom";
|
|
3
2
|
import { runOpenspecCapCommand } from "./openspec";
|
|
4
3
|
import { runIndexCapCommand } from "./index-cmd";
|
|
5
4
|
import { runPluginCliCommand } from "../../plugin-commands";
|
|
@@ -7,7 +6,7 @@ import { runPluginCliCommand } from "../../plugin-commands";
|
|
|
7
6
|
/**
|
|
8
7
|
* @command mate cap <capability> [...args]
|
|
9
8
|
* @description Dispatches to a capability-scoped subcommand: `graphify`,
|
|
10
|
-
* `
|
|
9
|
+
* `openspec` (each forwards `[...args]` to the underlying CLI tool,
|
|
11
10
|
* scoped to the resolved companion/working repo), or `index` (re-syncs
|
|
12
11
|
* capability indexes such as tokensave/graphify after code changes; `sync` is
|
|
13
12
|
* accepted as a compatibility alias for `index`). Names not claimed by the
|
|
@@ -21,7 +20,7 @@ export async function runCapCommand(
|
|
|
21
20
|
): Promise<void> {
|
|
22
21
|
if (!capabilityName) {
|
|
23
22
|
process.stderr.write(
|
|
24
|
-
"mate: capability name required. Use `mate cap <openspec|graphify|
|
|
23
|
+
"mate: capability name required. Use `mate cap <openspec|graphify|index|sync>`.\n",
|
|
25
24
|
);
|
|
26
25
|
process.exitCode = 1;
|
|
27
26
|
return;
|
|
@@ -31,9 +30,6 @@ export async function runCapCommand(
|
|
|
31
30
|
case "graphify":
|
|
32
31
|
await runGraphifyCapCommand(args);
|
|
33
32
|
return;
|
|
34
|
-
case "headroom":
|
|
35
|
-
await runHeadroomCapCommand(args);
|
|
36
|
-
return;
|
|
37
33
|
case "openspec":
|
|
38
34
|
await runOpenspecCapCommand(args);
|
|
39
35
|
return;
|
|
@@ -2,7 +2,8 @@ import { usage } from "../../usage";
|
|
|
2
2
|
import { runCompanionLinkCommand } from "./link";
|
|
3
3
|
import { runSetupCommand } from "../setup";
|
|
4
4
|
import { runCompanionListCommand } from "./list";
|
|
5
|
-
import {
|
|
5
|
+
import { runCompanionSyncCommand } from "./sync";
|
|
6
|
+
import { runWorkspaceOpenCommand } from "./open";
|
|
6
7
|
import { runCompanionTuiCommand } from "./tui";
|
|
7
8
|
|
|
8
9
|
export async function runCompanionCommand(
|
|
@@ -19,6 +20,9 @@ export async function runCompanionCommand(
|
|
|
19
20
|
case "list":
|
|
20
21
|
await runCompanionListCommand(argv);
|
|
21
22
|
return;
|
|
23
|
+
case "sync":
|
|
24
|
+
await runCompanionSyncCommand();
|
|
25
|
+
return;
|
|
22
26
|
case "open":
|
|
23
27
|
await runWorkspaceOpenCommand();
|
|
24
28
|
return;
|
|
@@ -239,8 +239,8 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
239
239
|
};
|
|
240
240
|
|
|
241
241
|
// Writes both the companion-side registry entry (companionRootedStore, the
|
|
242
|
-
// source of truth for
|
|
243
|
-
// (CompanionStore's internal dual write) in one call.
|
|
242
|
+
// source of truth for the aggregate pairing inventory) and the repo-local
|
|
243
|
+
// pointer (CompanionStore's internal dual write) in one call.
|
|
244
244
|
const registerRepository =
|
|
245
245
|
deps.registerRepository ??
|
|
246
246
|
((nextRepository, options) =>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
|
+
import { syncCompanionGit } from "../../../lib/orchestrator/companion-git-sync";
|
|
3
|
+
import { LaunchPreflightError } from "../../../lib/orchestrator/types";
|
|
4
|
+
import { resolveCompanionRuntime } from "../../../runtime/env";
|
|
5
|
+
import { readCompanionPolicy } from "../../../runtime/policy";
|
|
6
|
+
import { resolveCompanionPath, resolveRepoPath } from "./tui";
|
|
7
|
+
|
|
8
|
+
export interface CompanionSyncDeps {
|
|
9
|
+
resolveCompanionPath: typeof resolveCompanionPath;
|
|
10
|
+
resolveProjectedCompanionPath: (cwd: string) => string | undefined;
|
|
11
|
+
resolveRepoPath: typeof resolveRepoPath;
|
|
12
|
+
readCompanionPolicy: typeof readCompanionPolicy;
|
|
13
|
+
syncCompanionGit: typeof syncCompanionGit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const companionSyncDeps: CompanionSyncDeps = {
|
|
17
|
+
resolveCompanionPath,
|
|
18
|
+
resolveProjectedCompanionPath: (cwd) =>
|
|
19
|
+
resolveCompanionRuntime(process.env, cwd).context.companionPath || undefined,
|
|
20
|
+
resolveRepoPath,
|
|
21
|
+
readCompanionPolicy,
|
|
22
|
+
syncCompanionGit,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The Projection Root answers for a Wrapped Repository, which can never take
|
|
27
|
+
* the launch path — so the command must resolve there too, not only through the
|
|
28
|
+
* repo-local registry the launch uses.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveCompanion(cwd: string): Promise<string | undefined> {
|
|
31
|
+
const resolved = await companionSyncDeps.resolveCompanionPath(cwd);
|
|
32
|
+
if (resolved) return resolved;
|
|
33
|
+
return companionSyncDeps.resolveProjectedCompanionPath(cwd);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @command mate companion sync
|
|
38
|
+
* @description Runs the companion Git synchronization a launch preflight
|
|
39
|
+
* performs, against the companion resolved for the current directory. Runs Git
|
|
40
|
+
* interactively so credentials and conflicts can be answered.
|
|
41
|
+
*/
|
|
42
|
+
export async function runCompanionSyncCommand(): Promise<void> {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const companionPath = await resolveCompanion(cwd);
|
|
45
|
+
if (!companionPath) {
|
|
46
|
+
process.stderr.write(
|
|
47
|
+
`${FRAMEWORK_NAME}: no companion resolves for this directory. Run \`${FRAMEWORK_NAME} companion link\` first.\n`,
|
|
48
|
+
);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An explicit request does not override a companion configured not to have one. */
|
|
54
|
+
if (!companionSyncDeps.readCompanionPolicy(companionPath).gitAutoMode) {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
[
|
|
57
|
+
`${FRAMEWORK_NAME}: the companion's Git policy disables synchronization.`,
|
|
58
|
+
` companion: ${companionPath}`,
|
|
59
|
+
` Set \`git: auto\` in the companion's framework configuration to enable it.`,
|
|
60
|
+
"",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const repoPath = await companionSyncDeps.resolveRepoPath(cwd);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await companionSyncDeps.syncCompanionGit(
|
|
71
|
+
companionPath,
|
|
72
|
+
repoPath === companionPath ? undefined : repoPath,
|
|
73
|
+
true,
|
|
74
|
+
);
|
|
75
|
+
if (result.skipped) {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`${FRAMEWORK_NAME}: ${companionPath} is not a Git working tree; nothing to synchronize.\n`,
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(
|
|
82
|
+
`${FRAMEWORK_NAME}: synchronized ${companionPath} (${result.changed ? "updated" : "no changes"}).\n`,
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof LaunchPreflightError) {
|
|
86
|
+
process.stderr.write(`${error.message}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -103,9 +103,6 @@ function getSelectedToolChecks(config: {
|
|
|
103
103
|
if (selectedCapabilities.has("tokensave")) {
|
|
104
104
|
checks.push({ name: "tokensave", command: "tokensave", source: "capability" });
|
|
105
105
|
}
|
|
106
|
-
if (selectedCapabilities.has("headroom")) {
|
|
107
|
-
checks.push({ name: "headroom", command: "headroom", source: "capability" });
|
|
108
|
-
}
|
|
109
106
|
if (selectedCapabilities.has("rtk")) {
|
|
110
107
|
checks.push({ name: "rtk", command: "rtk", source: "capability" });
|
|
111
108
|
}
|
|
@@ -24,6 +24,20 @@ export interface ParsedLaunchArgs {
|
|
|
24
24
|
|
|
25
25
|
export { ensureUnambiguousCompanion, launchAmbiguityDeps };
|
|
26
26
|
|
|
27
|
+
export interface LaunchCommandDeps {
|
|
28
|
+
createLauncher: () => Pick<FrameworkLauncher, "prepare">;
|
|
29
|
+
createProgress: typeof createStartupProgress;
|
|
30
|
+
runIndexCapCommand: typeof runIndexCapCommand;
|
|
31
|
+
confirm: typeof confirm;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const launchCommandDeps: LaunchCommandDeps = {
|
|
35
|
+
createLauncher: () => new FrameworkLauncher(),
|
|
36
|
+
createProgress: createStartupProgress,
|
|
37
|
+
runIndexCapCommand,
|
|
38
|
+
confirm,
|
|
39
|
+
};
|
|
40
|
+
|
|
27
41
|
export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
28
42
|
const separatorIndex = argv.indexOf("--");
|
|
29
43
|
if (separatorIndex < 0) return { agentArgs: argv };
|
|
@@ -72,15 +86,19 @@ export async function runLaunchToolCommand(
|
|
|
72
86
|
skipGit?: boolean;
|
|
73
87
|
} = {},
|
|
74
88
|
): Promise<void> {
|
|
75
|
-
const
|
|
76
|
-
const progress =
|
|
89
|
+
const interactiveGit = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
90
|
+
const progress = interactiveGit
|
|
91
|
+
? launchCommandDeps.createProgress(`Starting ${tool}`)
|
|
92
|
+
: undefined;
|
|
77
93
|
|
|
78
94
|
try {
|
|
79
95
|
progress?.start("sync", STEP_LABELS.sync);
|
|
80
|
-
|
|
96
|
+
progress?.stop();
|
|
97
|
+
const prepared = await launchCommandDeps.createLauncher().prepare({
|
|
81
98
|
tool,
|
|
82
99
|
args,
|
|
83
100
|
skipGit: options.skipGit,
|
|
101
|
+
interactiveGit,
|
|
84
102
|
});
|
|
85
103
|
progress?.succeed("sync");
|
|
86
104
|
// Stop the Ink UI before running index steps: they spawn graphify/tokensave
|
|
@@ -89,14 +107,14 @@ export async function runLaunchToolCommand(
|
|
|
89
107
|
progress?.stop();
|
|
90
108
|
|
|
91
109
|
if (!options.skipConfirmation) {
|
|
92
|
-
const ok = await confirm("Continue? [y/N] ");
|
|
110
|
+
const ok = await launchCommandDeps.confirm("Continue? [y/N] ");
|
|
93
111
|
if (!ok) {
|
|
94
112
|
process.stderr.write("Aborted.\n");
|
|
95
113
|
process.exit(1);
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
116
|
|
|
99
|
-
await runIndexCapCommand([], {
|
|
117
|
+
await launchCommandDeps.runIndexCapCommand([], {
|
|
100
118
|
onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
|
|
101
119
|
onStepDone: (step, ok) => process.stdout.write(`${ok ? "✓" : "✗"} ${STEP_LABELS[step]}\n`),
|
|
102
120
|
});
|