@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.
Files changed (183) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -2
  2. package/claude-plugin/hooks/hooks.json +3 -6
  3. package/claude-plugin/hooks/session-guidance.mjs +8 -0
  4. package/claude-plugin/hooks/ts-loader.mjs +19 -0
  5. package/package.json +6 -4
  6. package/src/cli/commands/artifact/artifact.ts +19 -3
  7. package/src/cli/commands/artifact/finish/command.ts +183 -57
  8. package/src/cli/commands/artifact/finish/engine.ts +80 -91
  9. package/src/cli/commands/artifact/finish/finisher.ts +26 -33
  10. package/src/cli/commands/artifact/finish/git.ts +34 -23
  11. package/src/cli/commands/artifact/finish/index.ts +9 -3
  12. package/src/cli/commands/artifact/finish/openspec.ts +225 -97
  13. package/src/cli/commands/artifact/pending/command.ts +175 -0
  14. package/src/cli/commands/artifact/pending/discovery.ts +249 -0
  15. package/src/cli/commands/artifact/pending/index.ts +17 -0
  16. package/src/cli/commands/cap/index-cmd.ts +9 -1
  17. package/src/cli/commands/cap/index.ts +2 -6
  18. package/src/cli/commands/companion/companion.ts +5 -1
  19. package/src/cli/commands/companion/link.ts +2 -2
  20. package/src/cli/commands/companion/sync.ts +92 -0
  21. package/src/cli/commands/doctor.ts +0 -3
  22. package/src/cli/commands/launch/shared.ts +23 -5
  23. package/src/cli/commands/report/collector.ts +72 -78
  24. package/src/cli/commands/report/contract.ts +40 -1
  25. package/src/cli/commands/report/highlight.ts +27 -0
  26. package/src/cli/commands/report/index.ts +11 -18
  27. package/src/cli/commands/report/renderer.ts +199 -2
  28. package/src/cli/commands/report/types.ts +26 -1
  29. package/src/cli/commands/shared/companion-selection.ts +107 -10
  30. package/src/cli/commands/studio/areas.ts +68 -0
  31. package/src/cli/commands/studio/index.ts +69 -0
  32. package/src/cli/commands/studio/inventory.ts +55 -0
  33. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  34. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  35. package/src/cli/commands/studio/payload.ts +184 -0
  36. package/src/cli/commands/studio/routes.ts +2 -0
  37. package/src/cli/commands/studio/selection.ts +61 -0
  38. package/src/cli/commands/studio/server.ts +201 -0
  39. package/src/cli/commands/studio/snapshot.ts +63 -0
  40. package/src/cli/commands/studio/topology.ts +199 -0
  41. package/src/cli/commands/studio/views/client.ts +197 -0
  42. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  43. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  44. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  45. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  46. package/src/cli/commands/studio/views/document.tsx +256 -0
  47. package/src/cli/commands/studio/views/error.tsx +20 -0
  48. package/src/cli/commands/studio/views/model.ts +34 -0
  49. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  50. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  51. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  52. package/src/cli/commands/studio/views/styles.ts +389 -0
  53. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  54. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  56. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  57. package/src/cli/commands/unwrap.ts +70 -0
  58. package/src/cli/commands/wrap.ts +164 -0
  59. package/src/cli/main.ts +68 -19
  60. package/src/cli/parse-flags.ts +36 -11
  61. package/src/cli/usage.ts +12 -3
  62. package/src/framework.ts +1 -7
  63. package/src/hooks/session-banner.ts +64 -11
  64. package/src/hooks/session-guidance.ts +40 -0
  65. package/src/hooks/validate-artifact-path.ts +108 -35
  66. package/src/lib/fs-utils.ts +9 -0
  67. package/src/lib/install.ts +33 -0
  68. package/src/lib/orchestrator/adapters/base.ts +14 -125
  69. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  70. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  71. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  72. package/src/lib/orchestrator/config-store.ts +2 -21
  73. package/src/lib/orchestrator/editor.ts +12 -22
  74. package/src/lib/orchestrator/framework-context.ts +17 -6
  75. package/src/lib/orchestrator/global-config-store.ts +1 -1
  76. package/src/lib/orchestrator/launcher.ts +97 -7
  77. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  78. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  79. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  80. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  81. package/src/lib/orchestrator/projection-entries.ts +377 -0
  82. package/src/lib/orchestrator/projection-record.ts +56 -0
  83. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  84. package/src/lib/orchestrator/projection-types.ts +169 -0
  85. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  86. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  87. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  88. package/src/lib/orchestrator/types.ts +1 -0
  89. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  90. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  91. package/src/lib/package-paths.ts +11 -1
  92. package/src/opencode/companion-hooks.ts +89 -245
  93. package/src/opencode/companion-policy.ts +35 -10
  94. package/src/opencode/index.ts +1 -0
  95. package/src/opencode/projected-guidance.ts +56 -0
  96. package/src/opencode/tui.tsx +13 -4
  97. package/src/playbooks/companion-guidance.ts +32 -116
  98. package/src/plugins.ts +0 -1
  99. package/src/runtime/companion-git-state.ts +156 -0
  100. package/src/runtime/companion-git.ts +203 -0
  101. package/src/runtime/companion-guidance.ts +222 -0
  102. package/src/runtime/companion-sync.ts +298 -0
  103. package/src/runtime/env-names.ts +30 -0
  104. package/src/runtime/env.ts +67 -35
  105. package/src/runtime/framework.ts +10 -0
  106. package/src/runtime/freshness.ts +58 -0
  107. package/src/runtime/index.ts +104 -0
  108. package/src/runtime/install.ts +30 -0
  109. package/src/runtime/policy.ts +66 -0
  110. package/src/runtime/projected-guidance.ts +45 -0
  111. package/src/runtime/projection.ts +224 -0
  112. package/src/runtime/repo-local.ts +64 -0
  113. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  114. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  115. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  116. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  117. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  118. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  119. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  122. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  123. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  124. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  125. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  126. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  127. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  128. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  129. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  130. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  131. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
  132. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  133. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  134. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  135. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  136. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  137. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  138. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  139. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  140. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  141. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  142. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  143. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
  144. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  145. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  146. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  147. package/src/templates/report-assets/README.md +32 -0
  148. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  149. package/src/templates/report-assets/mermaid.min.js +4376 -0
  150. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  151. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  152. package/src/tools/setup/capabilities/graphify.ts +16 -7
  153. package/src/tools/setup/capabilities/openspec.ts +63 -56
  154. package/src/tools/setup/capabilities/tokensave.ts +47 -0
  155. package/src/tools/setup/engine.ts +34 -6
  156. package/src/tools/setup/mate.ts +42 -13
  157. package/src/tools/setup/plugin.ts +9 -0
  158. package/src/tools/setup/plugins/guidance.ts +11 -1
  159. package/src/tools/setup/providers/claude-format.ts +49 -4
  160. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  161. package/src/tools/setup/providers/claude.ts +55 -220
  162. package/src/tools/setup/providers/opencode.ts +41 -14
  163. package/src/tools/setup/runtime-documents.ts +174 -0
  164. package/src/tools/setup/surface-target.ts +50 -0
  165. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  166. package/src/tools/setup/working-repo-local-state.ts +21 -1
  167. package/src/tools/setup.ts +25 -3
  168. package/wrappers/bin/graphify +57 -8
  169. package/wrappers/bin/openspec +50 -3
  170. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  171. package/src/cli/commands/cap/headroom.ts +0 -52
  172. package/src/cli/commands/workspace/list.ts +0 -25
  173. package/src/cli/commands/workspace/materialize.ts +0 -46
  174. package/src/cli/commands/workspace/workspace.ts +0 -22
  175. package/src/hooks/artifact-finish-nudge.ts +0 -244
  176. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  177. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  178. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  179. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  180. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  181. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  182. package/src/tools/setup/capabilities/headroom.ts +0 -57
  183. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -1,4 +1,4 @@
1
1
  {
2
- "name": "mate",
3
- "description": "Mate-managed Claude Code hooks: artifact-path guardrail, session banner, and the OpenSpec archive-finish nudge. Ships inside @uniqbit/mate-core and is loaded per managed launch via --plugin-dir; it is never published separately."
2
+ "name": "mate-claude-plugin",
3
+ "description": "Mate-managed Claude Code hooks: artifact-path guardrail, session banner, and companion guidance. Ships inside @uniqbit/mate-core and is never published separately. A managed launch loads it with --plugin-dir; a wrapped repository instead inlines the guard and guidance commands into its settings, with ${CLAUDE_PLUGIN_ROOT} already resolved."
4
4
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Mate hooks — PreToolUse artifact-path guardrail, SessionStart banner, PostToolUse OpenSpec archive-finish nudge (self-gated at runtime)",
2
+ "description": "Mate hooks — PreToolUse artifact-path guardrail, SessionStart banner and companion guidance",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -20,15 +20,12 @@
20
20
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-banner.mjs\""
21
21
  }
22
22
  ]
23
- }
24
- ],
25
- "PostToolUse": [
23
+ },
26
24
  {
27
- "matcher": "Bash",
28
25
  "hooks": [
29
26
  {
30
27
  "type": "command",
31
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/artifact-finish-nudge.mjs\""
28
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-guidance.mjs\""
32
29
  }
33
30
  ]
34
31
  }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim: hook logic lives in src/hooks/session-guidance.ts, loaded through
3
+ // ts-loader.mjs because the installed plugin sits inside node_modules where
4
+ // node's native type stripping is disabled.
5
+ import "./ts-loader.mjs";
6
+
7
+ const { run } = await import("../../src/hooks/session-guidance.ts");
8
+ process.exitCode = run();
@@ -15,7 +15,26 @@ process.emitWarning = (warning, ...args) => {
15
15
  emitWarning.call(process, warning, ...args);
16
16
  };
17
17
 
18
+ // Source imports are extensionless, which node's ESM resolver refuses. Probe
19
+ // the TypeScript candidates the type-checker resolves so a hook module can
20
+ // import the runtime subpath the same way the rest of the source tree does.
21
+ const TS_CANDIDATE_SUFFIXES = [".ts", ".tsx", "/index.ts", "/index.tsx"];
22
+
18
23
  registerHooks({
24
+ resolve(specifier, context, next) {
25
+ if (!specifier.startsWith(".") || /\.[cm]?[jt]sx?$/.test(specifier)) {
26
+ return next(specifier, context);
27
+ }
28
+ const base = new URL(specifier, context.parentURL);
29
+ for (const suffix of TS_CANDIDATE_SUFFIXES) {
30
+ const candidate = new URL(`${base.href}${suffix}`);
31
+ if (fs.existsSync(fileURLToPath(candidate))) {
32
+ return { url: candidate.href, format: "module", shortCircuit: true };
33
+ }
34
+ }
35
+ return next(specifier, context);
36
+ },
37
+
19
38
  load(url, context, next) {
20
39
  if (!url.endsWith(".ts")) return next(url, context);
21
40
  const source = fs.readFileSync(fileURLToPath(url), "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.5",
3
+ "version": "0.16.0-canary.0",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -27,17 +27,19 @@
27
27
  "typecheck": "bunx tsc --noEmit -p ../../tsconfig.json",
28
28
  "lint": "oxlint src/",
29
29
  "lint:fix": "oxlint src/ --fix",
30
- "format": "oxfmt src/",
31
- "format:check": "oxfmt src/ --check"
30
+ "format": "oxfmt src/ \"!**/*.generated.*\" \"!**/report-assets/**\"",
31
+ "format:check": "oxfmt src/ \"!**/*.generated.*\" \"!**/report-assets/**\" --check"
32
32
  },
33
33
  "dependencies": {
34
34
  "@opencode-ai/plugin": "^1.18.3",
35
35
  "@opentui/core": "^0.4.5",
36
36
  "@opentui/keymap": "^0.4.5",
37
37
  "@opentui/solid": "^0.4.5",
38
+ "@tanstack/highlight": "0.0.10",
39
+ "hono": "^4.13.5",
38
40
  "ink": "^7.1.0",
39
41
  "react": "^19.2.7",
40
- "react-doctor": "0.8.1",
42
+ "react-doctor": "0.9.13",
41
43
  "semver": "^7.8.5",
42
44
  "yaml": "^2.9.0"
43
45
  },
@@ -1,5 +1,12 @@
1
1
  import { usage } from "../../usage";
2
- import { runArtifactFinishCommand } from "./finish";
2
+ import { runArtifactPublishCommand } from "./finish";
3
+ import { runArtifactPendingCommand } from "./pending";
4
+
5
+ /**
6
+ * Pre-rename spelling of `publish`. Kept so companions whose materialized skills were
7
+ * written before the rename keep working until they re-sync.
8
+ */
9
+ const DEPRECATED_PUBLISH_ALIAS = "finish";
3
10
 
4
11
  /**
5
12
  * @command mate artifact <subcommand>
@@ -11,8 +18,17 @@ export async function runArtifactCommand(
11
18
  argv: string[],
12
19
  ): Promise<void> {
13
20
  switch (subcommand) {
14
- case "finish":
15
- await runArtifactFinishCommand(argv);
21
+ case DEPRECATED_PUBLISH_ALIAS:
22
+ console.error(
23
+ "mate: `artifact finish` is deprecated and will be removed; use `artifact publish`.",
24
+ );
25
+ await runArtifactPublishCommand(argv);
26
+ return;
27
+ case "publish":
28
+ await runArtifactPublishCommand(argv);
29
+ return;
30
+ case "pending":
31
+ await runArtifactPendingCommand(argv);
16
32
  return;
17
33
  default:
18
34
  console.error(`Unknown artifact command: ${subcommand ?? ""}`);
@@ -2,37 +2,50 @@ import { resolveForCapability } from "../../../../lib/orchestrator/framework-con
2
2
  import type { LaunchContext } from "../../../../lib/orchestrator/framework-context";
3
3
  import type { CapabilityConfig } from "../../../../lib/orchestrator/types";
4
4
  import { WorkingRepoRequiredError } from "../../../../lib/orchestrator/types";
5
- import { parseFlags } from "../../../parse-flags";
5
+ import { type BooleanFlagSet, parseFlags } from "../../../parse-flags";
6
6
  import { ensureUnambiguousCompanion } from "../../shared/companion-selection";
7
- import { runFinishEngine } from "./engine";
8
- import type { FinishContext, FinisherFactory } from "./finisher";
7
+ import { discoverArchives, pendingArchives } from "../pending/discovery";
8
+ import { type FinishResult, runFinishEngine } from "./engine";
9
+ import type { ArtifactFinisher, FinishContext, FinisherFactory } from "./finisher";
9
10
  import { defaultGitOps, type GitOps } from "./git";
11
+ import { openspecSpecsFinisher, resolveDriftedSpecs } from "./openspec";
10
12
  import { DEFAULT_FINISHER_TYPE, knownFinisherTypes, selectFinisher } from "./registry";
11
13
 
12
- export interface FinishCommandDeps {
14
+ export interface PublishCommandDeps {
13
15
  ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
14
16
  resolveContext?: (cwd: string) => Promise<LaunchContext>;
15
17
  loadCapabilities?: (context: LaunchContext) => Promise<CapabilityConfig[]>;
16
18
  selectFinisher?: (type: string) => FinisherFactory | undefined;
17
19
  git?: (companionPath: string, workingRepoPath?: string) => GitOps;
20
+ /** Injectable clock; a spec publication's anchor is the calendar date it runs on. */
21
+ now?: () => Date;
22
+ /** Injectable spec-publication finisher; mirrors `selectFinisher` for the other unit. */
23
+ specsFinisher?: (context: FinishContext, paths: string[], now: () => Date) => ArtifactFinisher;
18
24
  stdout?: (line: string) => void;
19
25
  stderr?: (line: string) => void;
20
26
  }
21
27
 
22
- const VALUE_FLAGS = new Set(["--type"]);
28
+ /** Presence-only flags; every other `--flag` consumes the following token as its value. */
29
+ const BOOLEAN_FLAGS: BooleanFlagSet = new Set(["no-push", "json", "specs", "all"]);
23
30
 
24
- /** First non-flag token, skipping the value of any value-taking flag (e.g. `--type x`). */
25
- function positionalName(argv: string[]): string | undefined {
31
+ /**
32
+ * Every non-flag token, in order. Consumes the value of a value-taking flag exactly as
33
+ * {@link parseFlags} does, so the positionals and the flags can never disagree about
34
+ * which token belongs to whom.
35
+ */
36
+ function positionalNames(argv: string[]): string[] {
37
+ const names: string[] = [];
26
38
  for (let index = 0; index < argv.length; index += 1) {
27
39
  const token = argv[index];
28
- if (VALUE_FLAGS.has(token)) {
29
- index += 1;
40
+ if (!token.startsWith("--")) {
41
+ names.push(token);
30
42
  continue;
31
43
  }
32
- if (token.startsWith("--")) continue;
33
- return token;
44
+ const key = token.slice(2);
45
+ if (key.includes("=") || BOOLEAN_FLAGS.has(key)) continue;
46
+ index += 1;
34
47
  }
35
- return undefined;
48
+ return names;
36
49
  }
37
50
 
38
51
  async function defaultLoadCapabilities(context: LaunchContext): Promise<CapabilityConfig[]> {
@@ -40,41 +53,106 @@ async function defaultLoadCapabilities(context: LaunchContext): Promise<Capabili
40
53
  return config.capabilities ?? [];
41
54
  }
42
55
 
56
+ /** A publication the pipeline never ran: nothing was resolved, committed, tagged, or pushed. */
57
+ function nothingToPublish(name: string, message: string): FinishResult {
58
+ return {
59
+ type: DEFAULT_FINISHER_TYPE,
60
+ name,
61
+ anchorName: null,
62
+ tag: null,
63
+ resumed: false,
64
+ step: "done",
65
+ status: "skipped",
66
+ conflictedPaths: [],
67
+ local: { committed: false, tagged: false, pushed: false },
68
+ message,
69
+ };
70
+ }
71
+
72
+ function refusalResult(type: string, name: string, message: string): FinishResult {
73
+ return {
74
+ type,
75
+ name,
76
+ anchorName: null,
77
+ tag: null,
78
+ resumed: false,
79
+ step: "resolve",
80
+ status: "error",
81
+ conflictedPaths: [],
82
+ local: { committed: false, tagged: false, pushed: false },
83
+ message,
84
+ };
85
+ }
86
+
43
87
  /**
44
- * @command mate artifact finish <name>
45
- * @description Finishes and archives a completed artifact/change named `<name>`.
46
- * Resolves the launch context for the current working repo, loads the enabled
47
- * capabilities, selects a {@link FinisherFactory} for `--type` (defaulting to
48
- * `DEFAULT_FINISHER_TYPE`, e.g. an OpenSpec change), and — if that finisher is
49
- * enabled for this repo's capabilities — runs {@link runFinishEngine} to
50
- * validate, produce, and commit/push the finish artifacts via {@link GitOps}.
88
+ * @command mate artifact publish <target>
89
+ * @description Publishes an already-produced artifact. Resolves the launch context for the
90
+ * current working repo, loads the enabled capabilities, and runs {@link runFinishEngine} to
91
+ * commit/tag/push via {@link GitOps}. Two publication units exist: an archived change,
92
+ * named positionally, and a spec publication of drifted canonical specs, selected with
93
+ * `--specs`. Archiving is a precondition performed by the archive workflow; this command
94
+ * never archives.
51
95
  * @flags
52
- * - `<name>` — required positional: the change/artifact name to finish.
53
- * - `--type <type>` — finisher type to use; see `knownFinisherTypes()`.
54
- * - `--force` — bypass the completion guard; unrelated companion work is always preserved.
55
- * - `--no-push` — commit locally but skip pushing.
56
- * - `--json` — emit machine-readable JSON result instead of human-readable text.
57
- * @remarks No-ops (with a message on stderr) when the selected finisher is
58
- * disabled for the repo's configured capabilities — nothing is validated,
59
- * produced, or mutated in that case.
96
+ * - `<target>` — a dated archive anchor (`YYYY-MM-DD-<name>`), or a change name matching
97
+ * exactly one archive. Mutually exclusive with `--specs` and `--all`.
98
+ * - `--specs [<path>...]` — publish drifted canonical specs as their own publication under a
99
+ * new `openspec/specs/<date>-<specs>` tag. Bare, it publishes every drifted spec; with paths, only
100
+ * those. A bare name alongside it is a change target and is refused.
101
+ * - `--all` — publish every pending change in discovery order, then one spec publication for
102
+ * the drift that remains. Halts at the first conflict or error; with `--json` it emits a
103
+ * single array of results in execution order.
104
+ * - `--type <type>` — finisher type to use; see `knownFinisherTypes()`. `--specs` and
105
+ * `--all` are openspec-only.
106
+ * - `--no-push` — commit and tag locally but skip the remote sync and push.
107
+ * - `--json` — emit machine-readable JSON instead of human-readable text.
108
+ * @remarks No-ops (with a message on stderr) when the selected finisher is disabled for the
109
+ * repo's configured capabilities — nothing is resolved or mutated in that case.
60
110
  */
61
- export async function runArtifactFinishCommand(
111
+ export async function runArtifactPublishCommand(
62
112
  argv: string[],
63
- deps: FinishCommandDeps = {},
113
+ deps: PublishCommandDeps = {},
64
114
  ): Promise<void> {
65
115
  const emitOut = deps.stdout ?? ((line: string) => process.stdout.write(`${line}\n`));
66
116
  const emitErr = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`));
67
117
 
68
- const flags = parseFlags(argv);
69
- const force = flags.force === true;
118
+ const flags = parseFlags(argv, BOOLEAN_FLAGS);
70
119
  const noPush = flags["no-push"] === true;
71
120
  const json = flags.json === true;
72
121
  const type = typeof flags.type === "string" ? flags.type : DEFAULT_FINISHER_TYPE;
73
- const name = positionalName(argv);
122
+ const specsMode = flags.specs === true;
123
+ const allMode = flags.all === true;
124
+ const positionals = positionalNames(argv);
74
125
 
75
- if (!name) {
76
- emitErr("mate: artifact finish requires a change name.");
126
+ /** With `--specs`, a path narrows the publication; a bare name is a change target. */
127
+ const specPaths = specsMode ? positionals.filter((token) => token.includes("/")) : [];
128
+ const changeTargets = specsMode
129
+ ? positionals.filter((token) => !token.includes("/"))
130
+ : positionals;
131
+
132
+ /** Argument refusals go to stderr in every mode: nothing was resolved to report on. */
133
+ const refuse = (message: string): void => {
134
+ emitErr(message);
77
135
  process.exitCode = 1;
136
+ };
137
+
138
+ if (allMode && (specsMode || positionals.length > 0)) {
139
+ refuse(
140
+ "mate: --all already publishes both pending changes and drifted specs; drop the extra target.",
141
+ );
142
+ return;
143
+ }
144
+ if (specsMode && changeTargets.length > 0) {
145
+ refuse(
146
+ `mate: "${changeTargets[0]}" is a change target and --specs publishes canonical specs; they are different publication units. Pass one or the other.`,
147
+ );
148
+ return;
149
+ }
150
+ if (!allMode && !specsMode && positionals.length === 0) {
151
+ refuse("mate: artifact publish requires an archive anchor or change name, --specs, or --all.");
152
+ return;
153
+ }
154
+ if ((allMode || specsMode) && type !== DEFAULT_FINISHER_TYPE) {
155
+ refuse(`mate: --specs and --all are only available for the ${DEFAULT_FINISHER_TYPE} type.`);
78
156
  return;
79
157
  }
80
158
 
@@ -129,32 +207,80 @@ export async function runArtifactFinishCommand(
129
207
  context.repository?.path ?? process.env.MATE_REPO_PATH,
130
208
  );
131
209
  } catch (err) {
132
- const message = `mate: finish Git guard rejected the target: ${String(err)}`;
210
+ const message = `mate: publish Git guard rejected the target: ${String(err)}`;
211
+ if (json) emitOut(JSON.stringify(refusalResult(type, positionals[0] ?? "", message)));
212
+ else emitErr(message);
213
+ process.exitCode = 1;
214
+ return;
215
+ }
216
+
217
+ const now = deps.now ?? (() => new Date());
218
+ /**
219
+ * Under `--all --json` the array is the whole output, so the per-publication emission is
220
+ * suppressed; every other mode lets the engine report each publication as it finishes.
221
+ */
222
+ const quiet = allMode && json;
223
+ const engineDeps = {
224
+ git,
225
+ json: !allMode && json,
226
+ stdout: quiet ? (): void => {} : emitOut,
227
+ stderr: quiet ? (): void => {} : emitErr,
228
+ };
229
+
230
+ const runChange = (target: string): Promise<FinishResult> =>
231
+ runFinishEngine(finisher, { name: target, noPush }, engineDeps);
232
+
233
+ const runSpecs = async (requested: string[]): Promise<FinishResult | null> => {
234
+ const drift = await resolveDriftedSpecs(context.companionPath, git, requested);
235
+ if (!drift.ok) {
236
+ if (!quiet && !json) emitErr(drift.message);
237
+ process.exitCode = 1;
238
+ return refusalResult(type, "--specs", drift.message);
239
+ }
240
+ if (drift.paths.length === 0) return null;
241
+ const makeSpecsFinisher = deps.specsFinisher ?? openspecSpecsFinisher;
242
+ const specsFinisher = makeSpecsFinisher(finishContext, drift.paths, now);
243
+ return runFinishEngine(specsFinisher, { name: "--specs", noPush }, engineDeps);
244
+ };
245
+
246
+ const halted = (result: FinishResult): boolean =>
247
+ result.status === "conflict" || result.status === "error";
248
+
249
+ if (allMode) {
250
+ const results: FinishResult[] = [];
251
+ const changed = await git.changedPaths();
252
+ const changeKinds = (await git.changedPathKinds?.()) ?? {};
253
+ const archives = await discoverArchives(context.companionPath, changed, changeKinds);
254
+
255
+ for (const entry of pendingArchives(archives)) {
256
+ const result = await runChange(entry.anchor);
257
+ results.push(result);
258
+ if (halted(result)) {
259
+ if (json) emitOut(JSON.stringify(results));
260
+ return;
261
+ }
262
+ }
263
+
264
+ /** Drift is recomputed: a spec a change just committed is no longer drifted. */
265
+ const specResult = await runSpecs([]);
266
+ if (specResult) results.push(specResult);
267
+
268
+ if (json) emitOut(JSON.stringify(results));
269
+ else if (results.length === 0) emitOut("Nothing to publish.");
270
+ return;
271
+ }
272
+
273
+ if (specsMode) {
274
+ const result = await runSpecs(specPaths);
275
+ const emitted =
276
+ result ?? nothingToPublish("--specs", "No drifted canonical specs; nothing to publish.");
133
277
  if (json) {
134
- emitOut(
135
- JSON.stringify({
136
- type,
137
- name,
138
- anchorName: null,
139
- tag: null,
140
- resumed: false,
141
- step: "commit",
142
- status: "error",
143
- conflictedPaths: [],
144
- local: { committed: false, tagged: false, pushed: false },
145
- message,
146
- }),
147
- );
148
- } else {
149
- emitErr(message);
278
+ if (result === null) emitOut(JSON.stringify(emitted));
279
+ } else if (result === null) {
280
+ emitOut(emitted.message);
150
281
  }
151
- process.exitCode = 1;
152
282
  return;
153
283
  }
154
284
 
155
- await runFinishEngine(
156
- finisher,
157
- { name, force, noPush },
158
- { git, json, stdout: emitOut, stderr: emitErr },
159
- );
285
+ await runChange(positionals[0]);
160
286
  }