@uniqbit/mate-core 0.18.4 → 1.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -0,0 +1,174 @@
1
+ import type { ForgeAdapter, ForgeFailure, ForgeRepo, PullCandidate } from "./forge";
2
+ import {
3
+ pickProvenance,
4
+ sameRepository,
5
+ upsertDelivery,
6
+ type DeliveryRecord,
7
+ type MergeProvenance,
8
+ type MergeState,
9
+ } from "./record";
10
+
11
+ /** Why a branch's merge state must come from the user instead of the forge. */
12
+ export type AnswerReason =
13
+ | "no-token"
14
+ | "unsupported-host"
15
+ | "forge-failure"
16
+ | "no-pr"
17
+ | "ambiguous"
18
+ | "direct";
19
+
20
+ export interface BranchOutcome {
21
+ repository: string;
22
+ branch: string;
23
+ /** What is now known; `unknown` when the forge could not answer. */
24
+ result: MergeState | "ambiguous" | "none" | "unknown";
25
+ pr?: string;
26
+ candidates?: PullCandidate[];
27
+ failure?: ForgeFailure;
28
+ /** True when the user has to answer merged, not yet merged, or dropped. */
29
+ needsAnswer: boolean;
30
+ answerReason?: AnswerReason;
31
+ }
32
+
33
+ export type ForgeAccess =
34
+ | { adapter: ForgeAdapter; repo: ForgeRepo }
35
+ | { unavailable: "no-token" | "unsupported-host" };
36
+
37
+ /** The clock, forge, and local Git a refresh reads. */
38
+ export interface RefreshAccess {
39
+ now: () => Date;
40
+ forgeAccess: (repository: string) => Promise<ForgeAccess>;
41
+ }
42
+
43
+ export interface RefreshOptions extends RefreshAccess {
44
+ /** Restricts the refresh to one repository's delivery. */
45
+ repository?: string;
46
+ /** Shared across changes in one run, so a branch is asked about once. */
47
+ cache?: RefreshCache;
48
+ }
49
+
50
+ /** The PR number in a `…/pull/<n>` URL. */
51
+ function prNumberFrom(url: string | undefined): number | undefined {
52
+ const match = url ? /\/pull\/(\d+)/.exec(url) : null;
53
+ return match ? Number(match[1]) : undefined;
54
+ }
55
+
56
+ /**
57
+ * Asks the forge about every branch delivery of a record (or the one repository named)
58
+ * and records each definite answer with source `forge`. A direct delivery is never settled
59
+ * here: it always needs the user's answer. Nothing is recorded for an ambiguous,
60
+ * missing, or failed lookup: those come back as outcomes needing an answer. A delivery the
61
+ * user answered as merged or dropped is skipped.
62
+ */
63
+ export async function refreshRecord(
64
+ record: DeliveryRecord,
65
+ options: RefreshOptions,
66
+ ): Promise<{ record: DeliveryRecord; outcomes: BranchOutcome[] }> {
67
+ let next = record;
68
+ const outcomes: BranchOutcome[] = [];
69
+ const cache: RefreshCache = options.cache ?? new Map();
70
+
71
+ for (const delivery of record.deliveries) {
72
+ if (delivery.notApplicable !== undefined || !delivery.branch) continue;
73
+ if (options.repository && !sameRepository(delivery.repository, options.repository)) continue;
74
+ // A user answer of merged or dropped stands; the forge may still settle a "not yet merged".
75
+ if (delivery.merge?.source === "user" && delivery.merge.state !== "open") continue;
76
+ const { repository, branch } = delivery;
77
+ // A recorded PR URL picks that PR when several have the branch as head, unless it was dropped.
78
+ const prNumber = prNumberFrom(delivery.pr);
79
+ const direct = Boolean(delivery.direct);
80
+ // A direct delivery is checked in Git, a PR delivery on the forge: never share an answer.
81
+ const key = JSON.stringify([repository, branch, prNumber ?? null, direct]);
82
+
83
+ let outcome = cache.get(key);
84
+ if (!outcome) {
85
+ outcome = await lookup(repository, branch, direct, prNumber, options);
86
+ cache.set(key, outcome);
87
+ }
88
+ outcomes.push(outcome);
89
+
90
+ if (
91
+ outcome.result === "merged" ||
92
+ outcome.result === "open" ||
93
+ outcome.result === "closed-unmerged"
94
+ ) {
95
+ const merge = {
96
+ state: outcome.result,
97
+ source: "forge" as const,
98
+ checkedAt: options.now().toISOString(),
99
+ ...outcome.provenance,
100
+ };
101
+ next = upsertDelivery(next, repository, { merge, ...(outcome.pr ? { pr: outcome.pr } : {}) });
102
+ }
103
+ }
104
+ return { record: next, outcomes: outcomes.map(publicOutcome) };
105
+ }
106
+
107
+ /** Lookups by repository, branch, and PR number. */
108
+ export type RefreshCache = Map<string, LookupOutcome>;
109
+
110
+ interface LookupOutcome extends BranchOutcome {
111
+ provenance?: MergeProvenance;
112
+ }
113
+
114
+ function publicOutcome(outcome: LookupOutcome): BranchOutcome {
115
+ const { provenance: _provenance, ...rest } = outcome;
116
+ return rest;
117
+ }
118
+
119
+ async function lookup(
120
+ repository: string,
121
+ branch: string,
122
+ direct: boolean,
123
+ prNumber: number | undefined,
124
+ options: RefreshOptions,
125
+ ): Promise<LookupOutcome> {
126
+ const base = { repository, branch };
127
+ if (direct) {
128
+ // Git cannot tell this change's commits from the user's other pushes, so the user decides.
129
+ return { ...base, result: "unknown", needsAnswer: true, answerReason: "direct" };
130
+ }
131
+ const access = await options.forgeAccess(repository);
132
+ if ("unavailable" in access) {
133
+ return { ...base, result: "unknown", needsAnswer: true, answerReason: access.unavailable };
134
+ }
135
+ let answer = await access.adapter.pullsForBranch(access.repo, branch, prNumber);
136
+ // A recorded PR that is closed or gone must not hide a newer PR from the same branch.
137
+ if (
138
+ prNumber !== undefined &&
139
+ answer.ok &&
140
+ (answer.value.kind === "none" || answer.value.kind === "closed-unmerged")
141
+ ) {
142
+ const wider = await access.adapter.pullsForBranch(access.repo, branch);
143
+ if (wider.ok && wider.value.kind !== "none") answer = wider;
144
+ }
145
+ if (!answer.ok) {
146
+ return {
147
+ ...base,
148
+ result: "unknown",
149
+ failure: answer.failure,
150
+ needsAnswer: true,
151
+ answerReason: "forge-failure",
152
+ };
153
+ }
154
+ const value = answer.value;
155
+ if (value.kind === "none") {
156
+ return { ...base, result: "none", needsAnswer: true, answerReason: "no-pr" };
157
+ }
158
+ if (value.kind === "ambiguous") {
159
+ return {
160
+ ...base,
161
+ result: "ambiguous",
162
+ candidates: value.candidates,
163
+ needsAnswer: true,
164
+ answerReason: "ambiguous",
165
+ };
166
+ }
167
+ return {
168
+ ...base,
169
+ result: value.kind,
170
+ pr: value.pr.url,
171
+ needsAnswer: false,
172
+ provenance: pickProvenance(value.pr),
173
+ };
174
+ }
@@ -0,0 +1,97 @@
1
+ import { classifyBranch, type BranchProtection } from "./branches";
2
+ import { sameRepository } from "./record";
3
+ import type { DeliveryEnvironment } from "./environment";
4
+ import { forgeFor, forgeRepoOf, type ForgeDeps, type ForgeRepo } from "./forge";
5
+ import type { ForgeAccess } from "./refresh";
6
+ import { resolveForgeToken, type ForgeToken } from "./token";
7
+
8
+ /** What delivery needs to know about one repository, from local Git or the forge. */
9
+ export interface RepositoryAccess {
10
+ /** The Working Repository's from local Git; another's from the forge, else `main`. */
11
+ defaultBranch(): Promise<string>;
12
+ /** Protected when it is the default branch or matches a configured pattern. */
13
+ protection(branch: string): Promise<BranchProtection>;
14
+ forge(): Promise<ForgeAccess>;
15
+ }
16
+
17
+ export interface RepositoryAccessDeps {
18
+ resolveToken?: () => Promise<ForgeToken | null>;
19
+ forge?: ForgeDeps;
20
+ }
21
+
22
+ /**
23
+ * Access per repository, the Working Repository's through local Git first. The forge token
24
+ * is resolved at most once, and only when a lookup needs it.
25
+ */
26
+ export function repositoryAccessFor(
27
+ environment: DeliveryEnvironment,
28
+ deps: RepositoryAccessDeps = {},
29
+ ): (repository: string) => RepositoryAccess {
30
+ let token: Promise<ForgeToken | null> | undefined;
31
+ const resolveToken = (): Promise<ForgeToken | null> =>
32
+ (token ??= (deps.resolveToken ?? (() => resolveForgeToken()))());
33
+ const accesses = new Map<string, RepositoryAccess>();
34
+
35
+ return (repository) => {
36
+ const key = repository.toLowerCase();
37
+ let access = accesses.get(key);
38
+ if (!access) {
39
+ const slug = environment.workingRepo?.slug;
40
+ const working = slug && sameRepository(slug, repository) ? environment.workingRepo : null;
41
+ const forgeRepo = async (): Promise<ForgeRepo | null> =>
42
+ working?.forgeRepo ?? otherForgeRepo(environment, repository);
43
+ let forgeAccess: Promise<ForgeAccess> | undefined;
44
+ const forge = (): Promise<ForgeAccess> =>
45
+ (forgeAccess ??= connect(forgeRepo, resolveToken, deps.forge));
46
+ const defaultBranch = working ? () => working.git.defaultBranch() : forgeDefaultBranch(forge);
47
+ access = {
48
+ defaultBranch,
49
+ protection: async (branch) =>
50
+ classifyBranch(branch, {
51
+ defaultBranch: await defaultBranch(),
52
+ patterns: environment.protectedPatterns,
53
+ }),
54
+ forge,
55
+ };
56
+ accesses.set(key, access);
57
+ }
58
+ return access;
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Another repository's host comes from a linked checkout's remote, else the Working
64
+ * Repository's; never assumed. Null when no host is known.
65
+ */
66
+ async function otherForgeRepo(
67
+ environment: DeliveryEnvironment,
68
+ repository: string,
69
+ ): Promise<ForgeRepo | null> {
70
+ const host = (await environment.hostOf?.(repository)) ?? environment.workingRepo?.forgeRepo?.host;
71
+ return host ? forgeRepoOf(host, repository) : null;
72
+ }
73
+
74
+ async function connect(
75
+ forgeRepo: () => Promise<ForgeRepo | null>,
76
+ resolveToken: () => Promise<ForgeToken | null>,
77
+ deps: ForgeDeps | undefined,
78
+ ): Promise<ForgeAccess> {
79
+ const repo = await forgeRepo();
80
+ if (!repo) return { unavailable: "unsupported-host" };
81
+ const resolved = await resolveToken();
82
+ if (!resolved) return { unavailable: "no-token" };
83
+ const adapter = forgeFor(repo, resolved.token, deps);
84
+ return adapter ? { adapter, repo } : { unavailable: "unsupported-host" };
85
+ }
86
+
87
+ /** Another repository's default branch from the forge, asked once; `main` when it cannot answer. */
88
+ function forgeDefaultBranch(forge: () => Promise<ForgeAccess>): () => Promise<string> {
89
+ let known: Promise<string> | undefined;
90
+ return () =>
91
+ (known ??= (async () => {
92
+ const access = await forge();
93
+ if (!("adapter" in access)) return "main";
94
+ const answer = await access.adapter.defaultBranch(access.repo);
95
+ return answer.ok ? answer.value : "main";
96
+ })());
97
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The Delivery Path stage contract: the action-named sequence Studio and the docs check
3
+ * read. Review stages come from a schema's artifacts.
4
+ */
5
+
6
+ /** A skill stage runs a Mate skill; a human stage is a person reviewing one artifact. */
7
+ export type DeliveryStageKind = "skill" | "human";
8
+
9
+ interface StageBase {
10
+ /** Stable identifier, e.g. `archive` or `review-specs`. */
11
+ id: string;
12
+ /** The action the stage performs, e.g. `Archive change`. */
13
+ label: string;
14
+ kind: DeliveryStageKind;
15
+ /** Optional stages can be skipped and never block a later stage. */
16
+ optional: boolean;
17
+ }
18
+
19
+ export type SkillStageId =
20
+ | "clarify-intent"
21
+ | "explore"
22
+ | "propose"
23
+ | "apply"
24
+ | "inspect-outcome"
25
+ | "refine-code"
26
+ | "archive"
27
+ | "publish";
28
+
29
+ export interface SkillStage extends StageBase {
30
+ id: SkillStageId;
31
+ kind: "skill";
32
+ /** The Mate skill that drives the stage. */
33
+ skill: string;
34
+ }
35
+
36
+ export interface ReviewStage extends StageBase {
37
+ kind: "human";
38
+ skill: null;
39
+ /** The schema artifact the person reviews. */
40
+ artifact: string;
41
+ }
42
+
43
+ export type DeliveryStage = SkillStage | ReviewStage;
44
+
45
+ export interface DeliveryPath {
46
+ stages: DeliveryStage[];
47
+ /** False when the schema's artifacts could not be read, so reviews are unknown. */
48
+ reviewsKnown: boolean;
49
+ }
50
+
51
+ /** Artifact sets of the schemas Mate ships, in dependency order. */
52
+ export const SCHEMA_ARTIFACTS = {
53
+ "mate-v1": ["proposal", "specs", "design", "tasks"],
54
+ "mate-minimal": ["specs", "tasks"],
55
+ } as const satisfies Record<string, readonly string[]>;
56
+
57
+ /** A schema Mate ships. */
58
+ export type MateSchema = keyof typeof SCHEMA_ARTIFACTS;
59
+
60
+ /** The schema every companion is set to. */
61
+ export const DEFAULT_SCHEMA: MateSchema = "mate-v1";
62
+
63
+ export function isMateSchema(schema: string): schema is MateSchema {
64
+ return Object.hasOwn(SCHEMA_ARTIFACTS, schema);
65
+ }
66
+
67
+ function skillStage(id: SkillStageId, label: string, skill: string, optional = false): SkillStage {
68
+ return { id, label, kind: "skill", skill, optional };
69
+ }
70
+
71
+ const BEFORE_REVIEWS: readonly SkillStage[] = [
72
+ skillStage("clarify-intent", "Clarify intent", "mate-clarify-intent", true),
73
+ skillStage("explore", "Explore change", "mate-explore-change"),
74
+ skillStage("propose", "Propose change", "mate-propose-change"),
75
+ ];
76
+
77
+ const AFTER_REVIEWS: readonly SkillStage[] = [
78
+ skillStage("apply", "Apply change", "mate-apply-change"),
79
+ skillStage("inspect-outcome", "Inspect outcome", "mate-show-me", true),
80
+ skillStage("refine-code", "Refine code", "mate-simplify-code", true),
81
+ skillStage("archive", "Archive change", "mate-archive-change"),
82
+ skillStage("publish", "Publish companion artifacts", "mate-publish-artifact"),
83
+ ];
84
+
85
+ /** Every skill a Delivery Path stage is driven by, in stage order. */
86
+ export const DELIVERY_STAGE_SKILLS: readonly string[] = [...BEFORE_REVIEWS, ...AFTER_REVIEWS].map(
87
+ (stage) => stage.skill,
88
+ );
89
+
90
+ function reviewStage(artifact: string): ReviewStage {
91
+ return {
92
+ id: `review-${artifact}`,
93
+ label: `Review ${artifact}`,
94
+ kind: "human",
95
+ skill: null,
96
+ artifact,
97
+ optional: false,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * The Delivery Path for one schema. `artifacts` are its artifact ids in dependency order;
103
+ * `undefined` means the schema could not be read, which yields the skill stages alone with
104
+ * `reviewsKnown: false` rather than a guessed review set.
105
+ */
106
+ export function deliveryPath(artifacts: readonly string[] | undefined): DeliveryPath {
107
+ return {
108
+ stages: [...BEFORE_REVIEWS, ...(artifacts ?? []).map(reviewStage), ...AFTER_REVIEWS],
109
+ reviewsKnown: artifacts !== undefined,
110
+ };
111
+ }
@@ -0,0 +1,58 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ /** The only environment variable a forge token is read from. */
7
+ export const FORGE_TOKEN_ENV = "MATE_GIT_TOKEN";
8
+
9
+ export interface ForgeToken {
10
+ token: string;
11
+ source: typeof FORGE_TOKEN_ENV | "gh";
12
+ }
13
+
14
+ export interface ForgeTokenDeps {
15
+ env?: Record<string, string | undefined>;
16
+ /** `gh auth token`, or null when the GitHub CLI is missing or signed out. */
17
+ ghToken?: () => Promise<string | null>;
18
+ }
19
+
20
+ /** Variables `gh auth token` would report instead of the stored login. */
21
+ const GH_TOKEN_VARIABLES = [
22
+ "GH_TOKEN",
23
+ "GITHUB_TOKEN",
24
+ "GH_ENTERPRISE_TOKEN",
25
+ "GITHUB_ENTERPRISE_TOKEN",
26
+ ];
27
+
28
+ /** `env` without the token variables, so `gh` reports only its stored login. */
29
+ export function ghAuthEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
30
+ const scrubbed = { ...env };
31
+ for (const name of GH_TOKEN_VARIABLES) delete scrubbed[name];
32
+ return scrubbed;
33
+ }
34
+
35
+ async function defaultGhToken(): Promise<string | null> {
36
+ try {
37
+ const { stdout } = await execFileAsync("gh", ["auth", "token"], {
38
+ timeout: 10_000,
39
+ env: ghAuthEnv(process.env),
40
+ });
41
+ return stdout.trim() || null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The forge token from `MATE_GIT_TOKEN`, then `gh auth token`. Other variables such as
49
+ * `GITHUB_TOKEN` are deliberately ignored so a broadly scoped CI token is never picked up.
50
+ * The returned value is for the forge request only: callers never print, log, or store it.
51
+ */
52
+ export async function resolveForgeToken(deps: ForgeTokenDeps = {}): Promise<ForgeToken | null> {
53
+ const env = deps.env ?? process.env;
54
+ const fromEnv = env[FORGE_TOKEN_ENV]?.trim();
55
+ if (fromEnv) return { token: fromEnv, source: FORGE_TOKEN_ENV };
56
+ const fromGh = (await (deps.ghToken ?? defaultGhToken)())?.trim();
57
+ return fromGh ? { token: fromGh, source: "gh" } : null;
58
+ }
@@ -0,0 +1,18 @@
1
+ import { parse } from "yaml";
2
+
3
+ /** A leading `---` block; group 1 is its YAML, the match ends after the closing line. */
4
+ export const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
5
+
6
+ /** A Markdown file's leading YAML frontmatter; null when absent, malformed, or not a mapping. */
7
+ export function readFrontmatter(source: string): Record<string, unknown> | null {
8
+ const header = FRONTMATTER_PATTERN.exec(source)?.[1];
9
+ if (header === undefined) return null;
10
+ try {
11
+ const parsed: unknown = parse(header);
12
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
13
+ ? (parsed as Record<string, unknown>)
14
+ : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
@@ -40,11 +40,11 @@ export interface WarmPluginCacheResult {
40
40
 
41
41
  export const opencodePluginCacheDeps = {
42
42
  runInstall: (cwd: string, registry: string) =>
43
- spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent", "--registry", registry], {
44
- cwd,
45
- encoding: "utf8" as const,
46
- stdio: ["ignore", "pipe", "pipe"] as const,
47
- }),
43
+ spawnSync(
44
+ "npm",
45
+ ["install", "--no-audit", "--no-fund", "--loglevel=error", "--registry", registry],
46
+ { cwd, encoding: "utf8" as const, stdio: ["ignore", "pipe", "pipe"] as const },
47
+ ),
48
48
  };
49
49
 
50
50
  export async function warmOpenCodePackageCache(
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ /**
9
+ * Set on every `openspec` process Mate starts. Mate decides the CLI version, so
10
+ * OpenSpec's own "update available" hint is wrong advice here.
11
+ */
12
+ export const OPENSPEC_ENV = {
13
+ OPENSPEC_TELEMETRY: "0",
14
+ OPENSPEC_NO_UPDATE_CHECK: "1",
15
+ } as const satisfies Record<string, string>;
16
+
17
+ /** The bundled CLI, with its version when its manifest is readable, or the reinstall message. */
18
+ export type OpenSpecLocation = { bin: string; version?: string } | { missing: string };
19
+
20
+ export interface OpenSpecRunOptions {
21
+ cwd: string;
22
+ /** `capture` (default) collects output; `inherit` streams it to the terminal. */
23
+ stdio?: "capture" | "inherit";
24
+ /** Milliseconds before the child is killed. */
25
+ timeout?: number;
26
+ /** Base environment, `process.env` by default; {@link OPENSPEC_ENV} always wins. */
27
+ env?: NodeJS.ProcessEnv;
28
+ /** The CLI to run instead of the located one, e.g. when vendoring from a given CLI. */
29
+ bin?: string;
30
+ }
31
+
32
+ /**
33
+ * `code` is 1 when the CLI is missing or fails to start, with Mate's message as
34
+ * `stderr`; `null` when the child was killed. With `stdio: "inherit"` only that
35
+ * message is returned, the CLI's own output went to the terminal.
36
+ */
37
+ export interface OpenSpecRun {
38
+ code: number | null;
39
+ stdout: string;
40
+ stderr: string;
41
+ /** Set when the bundled CLI is missing, so nothing ran. */
42
+ missing?: true;
43
+ }
44
+
45
+ export type RunOpenSpec = (args: string[], options: OpenSpecRunOptions) => Promise<OpenSpecRun>;
46
+
47
+ function readVersion(bin: string): string | undefined {
48
+ try {
49
+ const manifest = path.resolve(path.dirname(bin), "../package.json");
50
+ return (JSON.parse(fs.readFileSync(manifest, "utf8")) as { version: string }).version;
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Finds the OpenSpec CLI shipped with mate-core the way Node resolves it, never
58
+ * an `openspec` on `PATH`. `resolve` exists for layout tests.
59
+ */
60
+ export function resolveOpenSpec(
61
+ resolve: (id: string) => string = require.resolve,
62
+ ): OpenSpecLocation {
63
+ let entryPath: string;
64
+ try {
65
+ // The package exports only `.` (dist/index.js); the binary sits beside dist/.
66
+ entryPath = resolve("@fission-ai/openspec");
67
+ } catch {
68
+ return { missing: "bundled OpenSpec CLI is missing. Reinstall Mate to restore it." };
69
+ }
70
+ const bin = path.resolve(path.dirname(entryPath), "../bin/openspec.js");
71
+ if (!fs.existsSync(bin)) {
72
+ return { missing: `bundled OpenSpec CLI is missing at ${bin}. Reinstall Mate to restore it.` };
73
+ }
74
+ return { bin, version: readVersion(bin) };
75
+ }
76
+
77
+ /** The one in-process seam: suites point `locate` at a stub CLI. */
78
+ export const openSpecCliDeps = {
79
+ locate: (): OpenSpecLocation => resolveOpenSpec(),
80
+ };
81
+
82
+ /** The CLI every OpenSpec process Mate starts runs. */
83
+ export function locateOpenSpec(): OpenSpecLocation {
84
+ return openSpecCliDeps.locate();
85
+ }
86
+
87
+ /** Runs the bundled CLI with {@link OPENSPEC_ENV} over the base environment. Never rejects. */
88
+ export function runOpenSpec(args: string[], options: OpenSpecRunOptions): Promise<OpenSpecRun> {
89
+ const location = options.bin ? { bin: options.bin } : locateOpenSpec();
90
+ if ("missing" in location) {
91
+ return Promise.resolve({ code: 1, stdout: "", stderr: location.missing, missing: true });
92
+ }
93
+ return new Promise((resolve) => {
94
+ // A JS entry runs on the current runtime, not its `#!/usr/bin/env node` shebang,
95
+ // which needs an executable bit and `node` on PATH and does not work on Windows.
96
+ const [command, commandArgs] = location.bin.endsWith(".js")
97
+ ? [process.execPath, [location.bin, ...args]]
98
+ : [location.bin, args];
99
+ const child = spawn(command, commandArgs, {
100
+ cwd: options.cwd,
101
+ env: { ...(options.env ?? process.env), ...OPENSPEC_ENV },
102
+ stdio: options.stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
103
+ ...(options.timeout === undefined ? {} : { timeout: options.timeout }),
104
+ });
105
+ let stdout = "";
106
+ let stderr = "";
107
+ child.stdout?.setEncoding("utf8");
108
+ child.stderr?.setEncoding("utf8");
109
+ child.stdout?.on("data", (chunk: string) => (stdout += chunk));
110
+ child.stderr?.on("data", (chunk: string) => (stderr += chunk));
111
+ child.once("error", (error) =>
112
+ resolve({ code: 1, stdout, stderr: `failed to run openspec: ${error.message}` }),
113
+ );
114
+ child.once("close", (code) => resolve({ code, stdout, stderr }));
115
+ });
116
+ }
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { parse } from "yaml";
5
+
6
+ import { FRONTMATTER_PATTERN } from "./frontmatter";
7
+
8
+ export const BOM = "";
9
+
10
+ export interface ScopePair {
11
+ repository: string;
12
+ area: string;
13
+ }
14
+
15
+ /**
16
+ * A leading `<!-- mate:delta … -->` comment; group 1 is its YAML, the match ends after
17
+ * the closing `-->` line. OpenSpec skips HTML comments, so a delta carrying its metadata
18
+ * here passes `validate --strict`.
19
+ */
20
+ export const DELTA_COMMENT_PATTERN =
21
+ /^<!--[ \t]*mate:delta[ \t]*\r?\n([\s\S]*?)\r?\n-->[ \t]*(?:\r?\n|$)/;
22
+
23
+ /** Reads `scopes` from a `mate:delta` comment, else from frontmatter (proposals, legacy deltas). */
24
+ export function parseScopes(source: string): ScopePair[] {
25
+ const text = source.startsWith(BOM) ? source.slice(BOM.length) : source;
26
+ const match = DELTA_COMMENT_PATTERN.exec(text) ?? FRONTMATTER_PATTERN.exec(text);
27
+ if (!match) return [];
28
+
29
+ try {
30
+ const parsed = parse(match[1]) as unknown;
31
+ if (!parsed || typeof parsed !== "object") return [];
32
+ const scopes = (parsed as Record<string, unknown>).scopes;
33
+ if (!Array.isArray(scopes)) return [];
34
+ return scopes.flatMap((entry) => {
35
+ if (!entry || typeof entry !== "object") return [];
36
+ const pair = entry as Record<string, unknown>;
37
+ return typeof pair.repository === "string" && typeof pair.area === "string"
38
+ ? [{ repository: pair.repository, area: pair.area }]
39
+ : [];
40
+ });
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ async function specFiles(directory: string): Promise<string[]> {
47
+ let entries;
48
+ try {
49
+ entries = await fs.readdir(directory, { withFileTypes: true });
50
+ } catch {
51
+ return [];
52
+ }
53
+ const nested = await Promise.all(
54
+ entries.map(async (entry) => {
55
+ const full = path.join(directory, entry.name);
56
+ if (entry.isDirectory()) return specFiles(full);
57
+ return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
58
+ }),
59
+ );
60
+ return nested.flat();
61
+ }
62
+
63
+ async function readOrEmpty(file: string): Promise<string> {
64
+ try {
65
+ return await fs.readFile(file, "utf8");
66
+ } catch {
67
+ return "";
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Repositories a change directory (active or archived) declares in its proposal
73
+ * frontmatter and delta `scopes`, deduplicated case-insensitively, first spelling kept.
74
+ */
75
+ export async function scopedRepositories(changeDir: string): Promise<string[]> {
76
+ const sources = [
77
+ await readOrEmpty(path.join(changeDir, "proposal.md")),
78
+ ...(await Promise.all((await specFiles(path.join(changeDir, "specs"))).map(readOrEmpty))),
79
+ ];
80
+ const seen = new Map<string, string>();
81
+ for (const pair of sources.flatMap(parseScopes)) {
82
+ const key = pair.repository.toLowerCase();
83
+ if (!seen.has(key)) seen.set(key, pair.repository);
84
+ }
85
+ return [...seen.values()];
86
+ }