@uniqbit/mate-core 0.18.5 → 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 (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -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
+ }
@@ -0,0 +1,50 @@
1
+ /** Reads the operator's `claude` arguments as tokens, for where Mate's appended prompt goes. */
2
+
3
+ export const APPEND_PROMPT_FLAG = "--append-system-prompt";
4
+
5
+ function flagArgs(args: readonly string[]): readonly string[] {
6
+ const terminator = args.indexOf("--");
7
+ return terminator === -1 ? args : args.slice(0, terminator);
8
+ }
9
+
10
+ /**
11
+ * The operator's own `--append-system-prompt` values, lifted out of `args`:
12
+ * Claude keeps only the last such flag, so each would replace Mate's Agent
13
+ * Guidance instead of following it. A trailing flag without a value
14
+ * stays, for Claude to reject. `boundary` is where a lifted flag stood before
15
+ * an argument that is no flag: a variadic option such as `--add-dir` before
16
+ * it would otherwise read that argument as its own value, so Mate's flag goes
17
+ * there. Only the first such place is kept: Claude reads one prompt.
18
+ */
19
+ export function liftAppendedPrompts(args: readonly string[]): {
20
+ args: string[];
21
+ prompts: string[];
22
+ boundary?: number;
23
+ } {
24
+ const flags = flagArgs(args);
25
+ const kept: string[] = [];
26
+ const prompts: string[] = [];
27
+ const liftedAt: number[] = [];
28
+ for (let index = 0; index < flags.length; index += 1) {
29
+ const arg = flags[index]!;
30
+ if (arg === APPEND_PROMPT_FLAG && index + 1 < flags.length) {
31
+ prompts.push(flags[index + 1]!);
32
+ liftedAt.push(kept.length);
33
+ index += 1;
34
+ } else if (arg.startsWith(`${APPEND_PROMPT_FLAG}=`)) {
35
+ prompts.push(arg.slice(APPEND_PROMPT_FLAG.length + 1));
36
+ liftedAt.push(kept.length);
37
+ } else {
38
+ kept.push(arg);
39
+ }
40
+ }
41
+ const passthrough = [...kept, ...args.slice(flags.length)];
42
+ const boundary = liftedAt.find(
43
+ (at) => at > 0 && at < passthrough.length && !passthrough[at]!.startsWith("-"),
44
+ );
45
+ return {
46
+ args: passthrough,
47
+ prompts,
48
+ ...(boundary === undefined ? {} : { boundary }),
49
+ };
50
+ }
@@ -5,23 +5,26 @@ import path from "node:path";
5
5
  import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import { buildCompanionGuidance } from "../../../playbooks/companion-guidance";
7
7
  import {
8
+ launchPluginFolders,
8
9
  planClaudeDelivery,
9
10
  type ClaudeDeliveryPlan,
10
- type SessionKind,
11
11
  } from "../../../runtime/claude-delivery-plan";
12
12
  import { hookGroupKey, type ClaudeHookGroup } from "../../../runtime/claude-hook-group";
13
+ import { resolveSecretPolicy } from "../../../runtime/policy";
14
+ import { managedSessionKind } from "../../../runtime/session-kind";
13
15
  import { NOT_WRAPPED } from "../../../runtime/wrap-state";
14
16
  import { writeAtomic } from "../../../runtime/write-atomic";
15
17
  import {
16
18
  getCompanionClaudeMcpConfigPath,
17
19
  getCompanionClaudeSettingsPath,
18
20
  } from "../../../tools/setup/providers/claude";
21
+ import { definesUserClaudeAttribution } from "../../../tools/setup/providers/claude-attribution";
19
22
  import type { ClaudeSettings } from "../../../tools/setup/providers/claude-format";
20
- import { getContextModePackageRoot, validateContextModePackage } from "../../context-mode-package";
21
23
  import { getClaudePluginRoot, validateClaudePluginAssets } from "../../package-paths";
22
24
  import type { PrepareResult } from "../session-prepare";
23
25
  import { LaunchPreflightError } from "../types";
24
26
  import { type AdapterContext, LaunchAdapter } from "./base";
27
+ import { APPEND_PROMPT_FLAG, liftAppendedPrompts } from "./claude-args";
25
28
 
26
29
  /** The paths are present only where the plan names that channel and the file exists. */
27
30
  interface ClaudeDelivery {
@@ -33,6 +36,8 @@ interface ClaudeDelivery {
33
36
  /** A Claude launch's context: the delivery its plan chose, and the files it chose or wrote. */
34
37
  interface ClaudeLaunchContext extends AdapterContext {
35
38
  delivery: ClaudeDelivery;
39
+ /** The Capability plugin folders this launch's own prepare gathered, in order. */
40
+ pluginDirectories: readonly string[];
36
41
  }
37
42
 
38
43
  /** Where a launch with a Working Repository writes its derived `--settings`; git-ignored. */
@@ -44,12 +49,15 @@ export function launchSettingsPath(companionPath: string, repositoryId: string):
44
49
  /**
45
50
  * Companion settings minus `exclude`, so each wrap-carried group runs once. A
46
51
  * file, not inline JSON (platform arg limits); per repository, so concurrent
47
- * launches never race.
52
+ * launches never race. `--settings` outranks every file scope, so `attribution`
53
+ * is dropped when the user or the repository sets its own, even one added
54
+ * after the companion's last sync.
48
55
  */
49
56
  async function writeLaunchSettings(
50
57
  companionSettingsPath: string,
51
58
  target: string,
52
59
  exclude: readonly ClaudeHookGroup[],
60
+ repoRoot: string,
53
61
  ): Promise<void> {
54
62
  const settings = JSON.parse(await fs.readFile(companionSettingsPath, "utf8")) as ClaudeSettings;
55
63
  const excluded = new Set(exclude.map(hookGroupKey));
@@ -60,6 +68,9 @@ async function writeLaunchSettings(
60
68
  }
61
69
  const derived: ClaudeSettings = { ...settings, hooks };
62
70
  if (Object.keys(hooks).length === 0) delete derived.hooks;
71
+ if (definesUserClaudeAttribution({ repoRoot })) {
72
+ delete derived.attribution;
73
+ }
63
74
 
64
75
  await fs.mkdir(path.dirname(target), { recursive: true });
65
76
  await writeAtomic(target, `${JSON.stringify(derived, null, 2)}\n`);
@@ -76,7 +87,12 @@ async function settingsPathFor(
76
87
  if (plan.hooks.owner === "settings") return companionSettingsPath;
77
88
  /** Owner `wrap` is planned only for `managed-repo`, which has a repository. */
78
89
  const settingsPath = launchSettingsPath(companionPath, repository!.id);
79
- await writeLaunchSettings(companionSettingsPath, settingsPath, plan.hooks.launchSettingsExclude);
90
+ await writeLaunchSettings(
91
+ companionSettingsPath,
92
+ settingsPath,
93
+ plan.hooks.launchSettingsExclude,
94
+ repository!.path,
95
+ );
80
96
  return settingsPath;
81
97
  }
82
98
 
@@ -91,7 +107,7 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
91
107
  */
92
108
  async planLaunch(context: AdapterContext, prepared: PrepareResult): Promise<ClaudeLaunchContext> {
93
109
  const { repository, companionPath } = context;
94
- const kind: SessionKind = repository ? "managed-repo" : "managed-companion";
110
+ const kind = managedSessionKind(Boolean(repository));
95
111
  const wrap = prepared.wrapState ?? NOT_WRAPPED;
96
112
  if (repository && !wrap.runtimes.has("claude")) {
97
113
  throw new LaunchPreflightError(
@@ -106,7 +122,7 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
106
122
  ...(plan.mcp === "mcp-config" && existsSync(mcpConfigPath) ? { mcpConfigPath } : {}),
107
123
  ...(settingsPath ? { settingsPath } : {}),
108
124
  };
109
- return { ...context, delivery };
125
+ return { ...context, delivery, pluginDirectories: prepared.claudePluginDirectories ?? [] };
110
126
  }
111
127
 
112
128
  /** A failed Claude wrap pass names the entry that failed. */
@@ -121,7 +137,7 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
121
137
  return super.launchFailure(prepared);
122
138
  }
123
139
 
124
- async validateLaunch(context: ClaudeLaunchContext): Promise<void> {
140
+ async validateLaunch(_context: ClaudeLaunchContext): Promise<void> {
125
141
  /** The bundled plugin carries the artifact-path guard; never launch without it. */
126
142
  try {
127
143
  validateClaudePluginAssets();
@@ -131,22 +147,14 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
131
147
  { cause: error },
132
148
  );
133
149
  }
134
-
135
- if (context.capabilities.some((capability) => capability.name === "context-mode")) {
136
- try {
137
- await validateContextModePackage(context.companionPath);
138
- } catch (error) {
139
- throw new Error(
140
- `Claude context-mode plugin is unavailable: ${(error as Error).message} Repair it with \`mate companion setup\`.`,
141
- { cause: error },
142
- );
143
- }
144
- }
145
150
  }
146
151
 
147
152
  /**
148
- * Plan in, flags out; decides nothing. `--append-system-prompt`, `--plugin-dir`
149
- * and context-mode are unconditional: no settings document can carry them.
153
+ * Plan in, flags out; decides nothing. `--append-system-prompt` is
154
+ * unconditional: no settings document can carry it. It holds the Agent
155
+ * Guidance alone; the Companion Instructions come from the wrap's session hook.
156
+ * `--plugin-dir` passes the folders `launchPluginFolders` names, from the
157
+ * running install's bundled plugin.
150
158
  */
151
159
  buildArgs(context: ClaudeLaunchContext, args: string[]): string[] {
152
160
  const { plan, settingsPath, mcpConfigPath } = context.delivery;
@@ -156,31 +164,31 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
156
164
  ? ["--setting-sources", "user,project,local", "--settings", settingsPath]
157
165
  : [];
158
166
  const addDirArgs = plan.dirAccess === "add-dir" ? ["--add-dir", context.companionPath] : [];
167
+ const pluginDirArgs = launchPluginFolders(
168
+ plan,
169
+ context.pluginDirectories,
170
+ getClaudePluginRoot(),
171
+ ).flatMap((dir) => ["--plugin-dir", dir]);
159
172
  const mcpConfigArgs =
160
173
  plan.mcp === "mcp-config" && mcpConfigPath ? ["--mcp-config", mcpConfigPath] : [];
161
- const contextModeArgs = context.capabilities.some(
162
- (capability) => capability.name === "context-mode",
163
- )
164
- ? ["--plugin-dir", getContextModePackageRoot(context.companionPath)]
165
- : [];
166
174
 
175
+ const guidance = buildCompanionGuidance({
176
+ ...context,
177
+ secretGuardrails: resolveSecretPolicy(context.companionPath).guardrails,
178
+ });
179
+ const { args: passthrough, prompts, boundary } = liftAppendedPrompts(args);
180
+ const promptArgs = [APPEND_PROMPT_FLAG, [guidance, ...prompts].filter(Boolean).join("\n\n")];
181
+ const launchArgs = [...settingsArgs, ...mcpConfigArgs, ...pluginDirArgs];
182
+
183
+ if (boundary === undefined) {
184
+ return [...addDirArgs, ...promptArgs, ...launchArgs, ...passthrough];
185
+ }
167
186
  return [
168
187
  ...addDirArgs,
169
- "--append-system-prompt",
170
- buildCompanionGuidance(context),
171
- ...settingsArgs,
172
- ...mcpConfigArgs,
173
- /** The bundled plugin from the running install, beside context-mode's. */
174
- "--plugin-dir",
175
- getClaudePluginRoot(),
176
- ...contextModeArgs,
177
- ...args,
188
+ ...launchArgs,
189
+ ...passthrough.slice(0, boundary),
190
+ ...promptArgs,
191
+ ...passthrough.slice(boundary),
178
192
  ];
179
193
  }
180
-
181
- extendEnvironment(_context: ClaudeLaunchContext): NodeJS.ProcessEnv {
182
- return {
183
- CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1",
184
- };
185
- }
186
194
  }
@@ -6,6 +6,7 @@ const path = await import("node:path");
6
6
 
7
7
  import { MATE_ENV } from "../../../runtime/env";
8
8
  import { validateGuidanceData, type MateGuidanceFile } from "../../../runtime/guidance";
9
+ import { resolveSecretPolicy } from "../../../runtime/policy";
9
10
 
10
11
  import { validateOpenCodePluginAssets } from "../../package-paths";
11
12
  import { readOpenCodeConfig } from "../../../tools/setup/providers/opencode-format";
@@ -118,6 +119,7 @@ export class OpenCodeAdapter extends LaunchAdapter {
118
119
  private buildGuidance(context: AdapterContext): MateGuidanceFile {
119
120
  return buildOpenCodeGuidance(context.capabilities, {
120
121
  companionScoped: !context.repository,
122
+ secretGuardrails: resolveSecretPolicy(context.companionPath).guardrails,
121
123
  });
122
124
  }
123
125