@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
|
@@ -3,10 +3,8 @@ import type { GitOps } from "./git";
|
|
|
3
3
|
|
|
4
4
|
/** Pipeline step a {@link FinishResult} refers to (generic across artifact kinds). */
|
|
5
5
|
export type FinishStep =
|
|
6
|
-
| "
|
|
7
|
-
| "
|
|
8
|
-
| "dirty-guard"
|
|
9
|
-
| "produce"
|
|
6
|
+
| "resolve"
|
|
7
|
+
| "branch-guard"
|
|
10
8
|
| "cap-sync"
|
|
11
9
|
| "commit"
|
|
12
10
|
| "sync-remote"
|
|
@@ -17,7 +15,7 @@ export type FinishStep =
|
|
|
17
15
|
export type FinishStatus = "ok" | "conflict" | "error" | "skipped";
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
|
-
* Machine-readable result emitted with `--json`. The
|
|
18
|
+
* Machine-readable result emitted with `--json`. The publish skill parses this to
|
|
21
19
|
* decide whether to hand a rebase conflict to a human, or to drive the remaining
|
|
22
20
|
* tag + push after resolving one.
|
|
23
21
|
*/
|
|
@@ -26,7 +24,7 @@ export interface FinishResult {
|
|
|
26
24
|
name: string;
|
|
27
25
|
anchorName: string | null;
|
|
28
26
|
tag: string | null;
|
|
29
|
-
/** True when
|
|
27
|
+
/** True when a prior publication already committed or tagged this anchor. */
|
|
30
28
|
resumed: boolean;
|
|
31
29
|
step: FinishStep;
|
|
32
30
|
status: FinishStatus;
|
|
@@ -37,7 +35,6 @@ export interface FinishResult {
|
|
|
37
35
|
|
|
38
36
|
export interface EngineOptions {
|
|
39
37
|
name: string;
|
|
40
|
-
force: boolean;
|
|
41
38
|
noPush: boolean;
|
|
42
39
|
}
|
|
43
40
|
|
|
@@ -49,12 +46,23 @@ export interface EngineDeps {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
* Runs the fixed
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
49
|
+
* Runs the fixed publication pipeline for a resolved {@link ArtifactFinisher}:
|
|
50
|
+
* resolve → branch guard → cap sync → scoped commit → sync-remote → tag → push. The
|
|
51
|
+
* git, remote-sync, and conflict-handoff machinery is identical for every artifact
|
|
52
|
+
* kind — only the finisher's resolution and cap sync vary.
|
|
53
|
+
*
|
|
54
|
+
* Isolation is a clean abort, not a rollback: the resolved artifact is durable input
|
|
55
|
+
* the pipeline never produced, so no step resets, restores, or deletes a path.
|
|
57
56
|
*/
|
|
57
|
+
/** Lowest free `<base>.N` (N from 2), probed against the tags that exist now. */
|
|
58
|
+
async function nextFreeTag(git: GitOps, base: string): Promise<string> {
|
|
59
|
+
for (let suffix = 2; suffix <= 1000; suffix += 1) {
|
|
60
|
+
const candidate = `${base}.${suffix}`;
|
|
61
|
+
if (!(await git.tagExists(candidate))) return candidate;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`exhausted ${base}.2 through ${base}.1000 without a free tag name`);
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
export async function runFinishEngine(
|
|
59
67
|
finisher: ArtifactFinisher,
|
|
60
68
|
options: EngineOptions,
|
|
@@ -68,7 +76,7 @@ export async function runFinishEngine(
|
|
|
68
76
|
anchorName: null,
|
|
69
77
|
tag: null,
|
|
70
78
|
resumed: false,
|
|
71
|
-
step: "
|
|
79
|
+
step: "resolve",
|
|
72
80
|
status: "ok",
|
|
73
81
|
conflictedPaths: [],
|
|
74
82
|
local: { committed: false, tagged: false, pushed: false },
|
|
@@ -93,101 +101,73 @@ export async function runFinishEngine(
|
|
|
93
101
|
process.exitCode = 1;
|
|
94
102
|
};
|
|
95
103
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!resuming) {
|
|
104
|
-
const validation = await finisher.validate(options.name);
|
|
105
|
-
if (!validation.valid) {
|
|
106
|
-
fail("validate", `mate: ${options.name} failed validation:\n${validation.errors.join("\n")}`);
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
if (!options.force) {
|
|
110
|
-
const completeness = await finisher.isComplete(options.name);
|
|
111
|
-
if (!completeness.complete) {
|
|
112
|
-
fail(
|
|
113
|
-
"complete-guard",
|
|
114
|
-
`mate: ${options.name} is not complete (${completeness.remaining} of ${completeness.total} tasks remaining). Use --force to override.`,
|
|
115
|
-
);
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
104
|
+
// Resolve the already-produced artifact. Nothing is mutated, so a refusal here — an
|
|
105
|
+
// unarchived target, or a name matching two archives — leaves the companion untouched.
|
|
106
|
+
result.step = "resolve";
|
|
107
|
+
const resolution = await finisher.resolve(options.name);
|
|
108
|
+
if (!resolution.ok) {
|
|
109
|
+
fail("resolve", resolution.message);
|
|
110
|
+
return result;
|
|
119
111
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`mate: ${finisher.type} produce failed: ${outcome.message}${
|
|
149
|
-
outcome.produced
|
|
150
|
-
? " Produced paths were restored."
|
|
151
|
-
: " Any partial output was retained for inspection."
|
|
152
|
-
}`,
|
|
153
|
-
);
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
produced = outcome.produced;
|
|
112
|
+
const resolved: Produced = resolution.resolved;
|
|
113
|
+
result.anchorName = resolved.anchorName;
|
|
114
|
+
const baseTag = `${resolved.tagNamespace ?? finisher.type}/${resolved.anchorName}`;
|
|
115
|
+
// Provisional: a `suffix` publication only learns its final name at the tag
|
|
116
|
+
// step, once the remote sync has revealed tags created elsewhere. Early exits
|
|
117
|
+
// therefore report the tag this run would have created, not one that exists.
|
|
118
|
+
result.tag = baseTag;
|
|
119
|
+
|
|
120
|
+
// Branch guard — before cap sync, so a refusal mutates nothing at all. It applies to
|
|
121
|
+
// --no-push too: the local tag it creates is the anchor a later push would publish.
|
|
122
|
+
result.step = "branch-guard";
|
|
123
|
+
const [expectedBranch, currentBranch] = await Promise.all([
|
|
124
|
+
git.defaultBranch(),
|
|
125
|
+
git.currentBranch(),
|
|
126
|
+
]);
|
|
127
|
+
if (currentBranch === null) {
|
|
128
|
+
fail(
|
|
129
|
+
"branch-guard",
|
|
130
|
+
`mate: publication requires the companion's default branch (${expectedBranch}); HEAD is detached.`,
|
|
131
|
+
);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
if (currentBranch !== expectedBranch) {
|
|
135
|
+
fail(
|
|
136
|
+
"branch-guard",
|
|
137
|
+
`mate: refusing to publish from ${currentBranch}; the companion's default branch is ${expectedBranch}.`,
|
|
138
|
+
);
|
|
139
|
+
return result;
|
|
157
140
|
}
|
|
158
|
-
result.anchorName = produced.anchorName;
|
|
159
|
-
const tagName = `${finisher.type}/${produced.anchorName}`;
|
|
160
|
-
result.tag = tagName;
|
|
161
141
|
|
|
162
142
|
// Capability sync (only openspec-derived outputs are refreshed; commit stays scoped).
|
|
163
143
|
if (finisher.capSync) {
|
|
164
144
|
result.step = "cap-sync";
|
|
165
145
|
if (!(await finisher.capSync())) {
|
|
166
|
-
// Post-produce failure: retain the produced artifact — it is the resume state.
|
|
167
146
|
fail(
|
|
168
147
|
"cap-sync",
|
|
169
|
-
"mate: cap sync failed; the
|
|
148
|
+
"mate: cap sync failed; the resolved archive was left in place — re-run `mate artifact publish` to retry.",
|
|
170
149
|
);
|
|
171
150
|
return result;
|
|
172
151
|
}
|
|
173
152
|
}
|
|
174
153
|
|
|
175
|
-
// Commit — stage only the finisher's scoped paths.
|
|
176
|
-
// already committed,
|
|
154
|
+
// Commit — stage only the finisher's scoped paths. Nothing to stage means a prior
|
|
155
|
+
// publication already committed this anchor, which is a resume rather than an error.
|
|
177
156
|
result.step = "commit";
|
|
178
157
|
try {
|
|
179
|
-
await git.add(
|
|
180
|
-
if (await git.hasStagedChanges(
|
|
158
|
+
await git.add(resolved.commitPaths);
|
|
159
|
+
if (await git.hasStagedChanges(resolved.commitPaths)) {
|
|
181
160
|
await git.commit(
|
|
182
|
-
`chore(${finisher.type}): finish ${
|
|
183
|
-
|
|
161
|
+
resolved.commitSubject ?? `chore(${finisher.type}): finish ${resolved.anchorName}`,
|
|
162
|
+
resolved.commitPaths,
|
|
184
163
|
);
|
|
164
|
+
} else {
|
|
165
|
+
result.resumed = true;
|
|
185
166
|
}
|
|
186
167
|
} catch (err) {
|
|
187
|
-
// Post-produce failure: retain the produced artifact — it is the resume state.
|
|
188
168
|
fail(
|
|
189
169
|
"commit",
|
|
190
|
-
`mate: commit failed: ${String(err)}; the
|
|
170
|
+
`mate: commit failed: ${String(err)}; the resolved archive was left in place — re-run \`mate artifact publish\` to retry.`,
|
|
191
171
|
);
|
|
192
172
|
return result;
|
|
193
173
|
}
|
|
@@ -219,12 +199,21 @@ export async function runFinishEngine(
|
|
|
219
199
|
}
|
|
220
200
|
}
|
|
221
201
|
|
|
222
|
-
// Tag the (rebased)
|
|
202
|
+
// Tag the (rebased) publication commit — idempotent if a prior run already tagged
|
|
203
|
+
// it. A `suffix` publication that produced a fresh commit takes the next free name
|
|
204
|
+
// instead, so its commit gets an anchor rather than trailing someone else's tag.
|
|
223
205
|
result.step = "tag";
|
|
206
|
+
let tagName = baseTag;
|
|
224
207
|
try {
|
|
225
|
-
if (!(await git.tagExists(
|
|
226
|
-
await git.tag(tagName, `
|
|
208
|
+
if (!(await git.tagExists(baseTag))) {
|
|
209
|
+
await git.tag(tagName, `Publish ${resolved.anchorName}`);
|
|
210
|
+
} else if (resolved.tagCollision === "suffix" && !result.resumed) {
|
|
211
|
+
tagName = await nextFreeTag(git, baseTag);
|
|
212
|
+
await git.tag(tagName, `Publish ${resolved.anchorName}`);
|
|
213
|
+
} else {
|
|
214
|
+
result.resumed = true;
|
|
227
215
|
}
|
|
216
|
+
result.tag = tagName;
|
|
228
217
|
} catch (err) {
|
|
229
218
|
// Post-commit failure: retain the commit, do not roll back.
|
|
230
219
|
fail("tag", `mate: tag failed: ${String(err)}`);
|
|
@@ -235,7 +224,7 @@ export async function runFinishEngine(
|
|
|
235
224
|
if (options.noPush) {
|
|
236
225
|
result.step = "done";
|
|
237
226
|
result.status = "skipped";
|
|
238
|
-
result.message = `
|
|
227
|
+
result.message = `Published ${resolved.anchorName} locally (commit + tag ${tagName}); not pushed (--no-push).`;
|
|
239
228
|
emit();
|
|
240
229
|
return result;
|
|
241
230
|
}
|
|
@@ -255,7 +244,7 @@ export async function runFinishEngine(
|
|
|
255
244
|
|
|
256
245
|
result.step = "done";
|
|
257
246
|
result.status = "ok";
|
|
258
|
-
result.message = `
|
|
247
|
+
result.message = `Published ${resolved.anchorName}: ${result.resumed ? "resumed, " : ""}committed, tagged ${tagName}, and pushed.`;
|
|
259
248
|
emit();
|
|
260
249
|
return result;
|
|
261
250
|
}
|
|
@@ -7,38 +7,38 @@ export interface FinishContext {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* The committable result of a
|
|
11
|
-
* derived from and which paths the
|
|
10
|
+
* The committable result of resolving a publication target: what anchor the tag is
|
|
11
|
+
* derived from and which paths the publication commit is scoped to.
|
|
12
12
|
*/
|
|
13
13
|
export interface Produced {
|
|
14
14
|
/** Dated/immutable anchor the tag mirrors, e.g. `2026-07-14-my-change`. */
|
|
15
15
|
anchorName: string;
|
|
16
|
-
/** Pathspecs the
|
|
16
|
+
/** Pathspecs the publication commit stages — and nothing outside them. */
|
|
17
17
|
commitPaths: string[];
|
|
18
|
+
/** Tag namespace preceding the anchor; defaults to the finisher's type. */
|
|
19
|
+
tagNamespace?: string;
|
|
20
|
+
/** Whole commit subject; defaults to `chore(<type>): finish <anchorName>`. */
|
|
21
|
+
commitSubject?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Tag behaviour when the name is taken and this run produced a fresh commit.
|
|
24
|
+
* `reuse` (default) leaves the existing tag and pushes; `suffix` takes the
|
|
25
|
+
* lowest free `.N` so the fresh commit gets an anchor of its own. A resumed
|
|
26
|
+
* run reuses under either value: its commit is the tagged one already.
|
|
27
|
+
*/
|
|
28
|
+
tagCollision?: "reuse" | "suffix";
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
errors: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CompleteResult {
|
|
26
|
-
complete: boolean;
|
|
27
|
-
total: number;
|
|
28
|
-
remaining: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ProduceResult {
|
|
32
|
-
ok: boolean;
|
|
33
|
-
produced: Produced | null;
|
|
34
|
-
message: string;
|
|
35
|
-
}
|
|
31
|
+
/** Outcome of {@link ArtifactFinisher.resolve}; the failure carries the user-facing refusal. */
|
|
32
|
+
export type ResolveResult = { ok: true; resolved: Produced } | { ok: false; message: string };
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
|
-
* The variable, per-artifact-kind half of `mate artifact
|
|
39
|
-
* ({@link ../engine}) owns everything type-agnostic —
|
|
40
|
-
*
|
|
41
|
-
*
|
|
35
|
+
* The variable, per-artifact-kind half of `mate artifact publish`. The engine
|
|
36
|
+
* ({@link ../engine}) owns everything type-agnostic — branch guard, commit,
|
|
37
|
+
* remote-sync, conflict handoff, tag, push. A finisher supplies only what differs
|
|
38
|
+
* between artifact kinds (openspec changes today; ADRs, etc. later).
|
|
39
|
+
*
|
|
40
|
+
* Producing the artifact is not part of the interface: the archive-equivalent step
|
|
41
|
+
* for every artifact kind is its own workflow, and publishing is terminal over it.
|
|
42
42
|
*/
|
|
43
43
|
export interface ArtifactFinisher {
|
|
44
44
|
/** Selector key, e.g. `openspec`. */
|
|
@@ -47,18 +47,11 @@ export interface ArtifactFinisher {
|
|
|
47
47
|
readonly disabledReason: string;
|
|
48
48
|
/** Gate: is this finisher usable in the resolved capability set? */
|
|
49
49
|
isEnabled(capabilities: CapabilityConfig[]): boolean;
|
|
50
|
-
/** Never-bypassable guard. Not run when resuming an already-produced artifact. */
|
|
51
|
-
validate(name: string): Promise<ValidateResult>;
|
|
52
|
-
/** `--force`-overridable guard. Not run when resuming. */
|
|
53
|
-
isComplete(name: string): Promise<CompleteResult>;
|
|
54
50
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* {@link produce} and continues to commit → tag → push, or null if not yet produced.
|
|
51
|
+
* Resolve a target — a dated anchor or an unambiguous artifact name — to the already
|
|
52
|
+
* produced outputs. Mutates nothing a failure would have to undo.
|
|
58
53
|
*/
|
|
59
|
-
|
|
60
|
-
/** The terminal transform that yields committable outputs (openspec archive; …). */
|
|
61
|
-
produce(name: string): Promise<ProduceResult>;
|
|
54
|
+
resolve(target: string): Promise<ResolveResult>;
|
|
62
55
|
/** Optional capability sync scoped to this finisher; resolves false on failure. */
|
|
63
56
|
capSync?(): Promise<boolean>;
|
|
64
57
|
}
|
|
@@ -12,15 +12,21 @@ export interface PushResult {
|
|
|
12
12
|
error: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export type WorkingTreeChange = "new" | "modified";
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Git operations the finish engine needs, injectable for deterministic tests. All
|
|
17
19
|
* operations run against the companion working tree.
|
|
18
20
|
*/
|
|
19
21
|
export interface GitOps {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
+
/** Checked-out branch name, or null in a detached HEAD. */
|
|
23
|
+
currentBranch(): Promise<string | null>;
|
|
24
|
+
/** Branch publication is allowed on: remote HEAD, then `init.defaultBranch`, then `main`. */
|
|
25
|
+
defaultBranch(): Promise<string>;
|
|
22
26
|
/** Paths with uncommitted changes (porcelain), for scope-aware guards. */
|
|
23
27
|
changedPaths(): Promise<string[]>;
|
|
28
|
+
/** Git change kind by path, for consumers that need new versus modified metadata. */
|
|
29
|
+
changedPathKinds?(): Promise<Record<string, WorkingTreeChange>>;
|
|
24
30
|
/** Paths already present in the index. */
|
|
25
31
|
stagedPaths(): Promise<string[]>;
|
|
26
32
|
/** Stage the given pathspecs. */
|
|
@@ -29,8 +35,6 @@ export interface GitOps {
|
|
|
29
35
|
hasStagedChanges(paths?: string[]): Promise<boolean>;
|
|
30
36
|
/** Commit only the supplied pathspecs, leaving unrelated staged work untouched. */
|
|
31
37
|
commit(message: string, paths?: string[]): Promise<void>;
|
|
32
|
-
/** Restore only produced artifact paths to a recorded ref. */
|
|
33
|
-
restorePaths(ref: string, paths: string[]): Promise<void>;
|
|
34
38
|
hasUpstream(): Promise<boolean>;
|
|
35
39
|
fetch(): Promise<void>;
|
|
36
40
|
rebaseOntoUpstream(): Promise<RebaseResult>;
|
|
@@ -98,6 +102,11 @@ function parsePorcelainPath(line: string): string {
|
|
|
98
102
|
return arrow === -1 ? body : body.slice(arrow + 4);
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
function parsePorcelainKind(line: string): WorkingTreeChange {
|
|
106
|
+
const status = line.slice(0, 2);
|
|
107
|
+
return status.includes("?") || status.includes("A") ? "new" : "modified";
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
export function defaultGitOps(
|
|
102
111
|
companionPath: string,
|
|
103
112
|
workingRepoPath = process.env.MATE_REPO_PATH,
|
|
@@ -112,14 +121,33 @@ export function defaultGitOps(
|
|
|
112
121
|
return res;
|
|
113
122
|
};
|
|
114
123
|
return {
|
|
115
|
-
async
|
|
116
|
-
|
|
124
|
+
async currentBranch() {
|
|
125
|
+
const res = exec(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
126
|
+
return res.status === 0 && res.out.length > 0 ? res.out : null;
|
|
127
|
+
},
|
|
128
|
+
async defaultBranch() {
|
|
129
|
+
// A configured remote HEAD is the only answer the remote itself asserts; the
|
|
130
|
+
// rest are local conventions, narrowing to git's own default last.
|
|
131
|
+
const remoteHead = exec(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
132
|
+
if (remoteHead.status === 0 && remoteHead.out.length > 0) {
|
|
133
|
+
return remoteHead.out.replace(/^origin\//, "");
|
|
134
|
+
}
|
|
135
|
+
const configured = exec(["config", "--get", "init.defaultBranch"]);
|
|
136
|
+
if (configured.status === 0 && configured.out.length > 0) return configured.out;
|
|
137
|
+
return "main";
|
|
117
138
|
},
|
|
118
139
|
async changedPaths() {
|
|
119
140
|
const out = exec(["status", "--porcelain"]).out;
|
|
120
141
|
if (out.length === 0) return [];
|
|
121
142
|
return out.split("\n").map(parsePorcelainPath);
|
|
122
143
|
},
|
|
144
|
+
async changedPathKinds() {
|
|
145
|
+
const out = exec(["status", "--porcelain"]).out;
|
|
146
|
+
if (out.length === 0) return {};
|
|
147
|
+
return Object.fromEntries(
|
|
148
|
+
out.split("\n").map((line) => [parsePorcelainPath(line), parsePorcelainKind(line)]),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
123
151
|
async stagedPaths() {
|
|
124
152
|
const out = exec(["diff", "--cached", "--name-only"]).out;
|
|
125
153
|
return out.length === 0 ? [] : out.split("\n");
|
|
@@ -153,23 +181,6 @@ export function defaultGitOps(
|
|
|
153
181
|
if (paths && paths.length > 0 && known.length === 0) return;
|
|
154
182
|
execOrThrow(["commit", "-m", message, ...pathspec]);
|
|
155
183
|
},
|
|
156
|
-
async restorePaths(ref, paths) {
|
|
157
|
-
const tracked = paths.filter(
|
|
158
|
-
(filePath) => exec(["ls-files", "--cached", "--", filePath]).out.length > 0,
|
|
159
|
-
);
|
|
160
|
-
if (tracked.length > 0) {
|
|
161
|
-
execOrThrow(["restore", "--source", ref, "--staged", "--worktree", "--", ...tracked]);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// `git restore` does not remove newly-created untracked archive files.
|
|
165
|
-
const untracked = paths.filter(
|
|
166
|
-
(filePath) =>
|
|
167
|
-
exec(["ls-files", "--others", "--exclude-standard", "--", filePath]).out.length > 0,
|
|
168
|
-
);
|
|
169
|
-
if (untracked.length > 0) {
|
|
170
|
-
execOrThrow(["clean", "-fd", "--", ...untracked]);
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
184
|
async hasUpstream() {
|
|
174
185
|
return exec(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).status === 0;
|
|
175
186
|
},
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { runArtifactPublishCommand } from "./command";
|
|
2
|
+
export type { PublishCommandDeps } from "./command";
|
|
3
3
|
export type { FinishResult, FinishStep, FinishStatus } from "./engine";
|
|
4
|
-
export type {
|
|
4
|
+
export type {
|
|
5
|
+
ArtifactFinisher,
|
|
6
|
+
FinishContext,
|
|
7
|
+
FinisherFactory,
|
|
8
|
+
Produced,
|
|
9
|
+
ResolveResult,
|
|
10
|
+
} from "./finisher";
|