@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.
- package/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -12,12 +12,15 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
+
import { artifactRefusal, resolveArtifactTarget } from "../runtime/artifact-refusal";
|
|
16
|
+
import { checkCommitAttribution } from "../runtime/commit-attribution-guard";
|
|
15
17
|
import { gitEnvironment } from "../runtime/companion-git";
|
|
16
18
|
import { companionForkRefusal } from "../runtime/companion-sync";
|
|
17
19
|
import { readCompanionRuntimeContext } from "../runtime/env";
|
|
18
20
|
import { companionLinkPath } from "../runtime/repo-local";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
import { checkSecretAccess, type SecretToolCall } from "../runtime/secret-guard";
|
|
22
|
+
import { stripHeredocBodies } from "../runtime/shell-heredoc";
|
|
23
|
+
import type { HookEnv } from "./hook-io";
|
|
21
24
|
|
|
22
25
|
export interface HookOutcome {
|
|
23
26
|
exitCode: number;
|
|
@@ -44,8 +47,9 @@ const ARTIFACT_PATH_MARKERS = [
|
|
|
44
47
|
"/docs/prd/",
|
|
45
48
|
];
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
const
|
|
50
|
+
/** `[ \t]`, not `\s`: a redirect or `tee` names its target on its own line. */
|
|
51
|
+
const MD_REDIRECT_PATTERN = />{1,2}[ \t]*([^\s;|&<>]+\.md)\b/g;
|
|
52
|
+
const MD_TEE_PATTERN = /\btee[ \t]+(?:-a[ \t]+)?([^\s;|&<>]+\.md)\b/g;
|
|
49
53
|
|
|
50
54
|
export function artifactLikePath(filePath: string): boolean {
|
|
51
55
|
if (!filePath) return false;
|
|
@@ -118,11 +122,6 @@ function claudeConfigRoot(env: HookEnv): string {
|
|
|
118
122
|
return path.normalize(path.join(env.HOME || os.homedir(), ".claude"));
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
function normalizePath(filePath: string, ctx: GuardContext): string {
|
|
122
|
-
if (path.isAbsolute(filePath)) return path.normalize(filePath);
|
|
123
|
-
return path.normalize(path.join(ctx.repoRoot, filePath));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
125
|
function isUnder(target: string, root: string): boolean {
|
|
127
126
|
const relative = path.relative(path.normalize(root), path.normalize(target));
|
|
128
127
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -181,17 +180,14 @@ function isProductDocumentationPath(target: string, ctx: GuardContext): boolean
|
|
|
181
180
|
return false;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
function blockWrite(target: string,
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
lines.push(`Note: CLAUDE.md already lives in the companion repo at ${companion}/CLAUDE.md.`);
|
|
193
|
-
}
|
|
194
|
-
return { exitCode: 2, stderr: lines.join("\n") + "\n" };
|
|
183
|
+
function blockWrite(target: string, resolved: string, ctx: GuardContext): HookOutcome {
|
|
184
|
+
const refusal = artifactRefusal({
|
|
185
|
+
target,
|
|
186
|
+
resolved,
|
|
187
|
+
companion: ctx.companion,
|
|
188
|
+
repoRoot: ctx.repoRoot,
|
|
189
|
+
});
|
|
190
|
+
return { exitCode: 2, stderr: `${refusal}\n` };
|
|
195
191
|
}
|
|
196
192
|
|
|
197
193
|
function checkFilePath(
|
|
@@ -201,7 +197,7 @@ function checkFilePath(
|
|
|
201
197
|
): HookOutcome | null {
|
|
202
198
|
if (!filePath) return null;
|
|
203
199
|
|
|
204
|
-
const normalized =
|
|
200
|
+
const normalized = resolveArtifactTarget(filePath, ctx.repoRoot);
|
|
205
201
|
if (isCompanionPath(normalized, ctx)) return null;
|
|
206
202
|
|
|
207
203
|
if (isUnder(normalized, claudeConfigRoot(ctx.env))) return null;
|
|
@@ -219,7 +215,7 @@ function checkFilePath(
|
|
|
219
215
|
return null;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
return blockWrite(filePath, ctx
|
|
218
|
+
return blockWrite(filePath, normalized, ctx);
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
/**
|
|
@@ -234,17 +230,19 @@ async function checkForkedCompanion(
|
|
|
234
230
|
ctx: GuardContext,
|
|
235
231
|
): Promise<HookOutcome | null> {
|
|
236
232
|
if (!filePath || !artifactLikePath(filePath)) return null;
|
|
237
|
-
if (!isCompanionPath(
|
|
233
|
+
if (!isCompanionPath(resolveArtifactTarget(filePath, ctx.repoRoot), ctx)) return null;
|
|
238
234
|
|
|
239
235
|
const refusal = await companionForkRefusal(ctx.env, ctx.companion);
|
|
240
236
|
return refusal ? { exitCode: 2, stderr: `${refusal}\n` } : null;
|
|
241
237
|
}
|
|
242
238
|
|
|
239
|
+
/** Heredoc bodies are what a command writes, not what it runs: they are not scanned. */
|
|
243
240
|
export function commandMatches(command: string): string[] {
|
|
244
241
|
const targets: string[] = [];
|
|
242
|
+
const commandText = stripHeredocBodies(command);
|
|
245
243
|
for (const pattern of [MD_REDIRECT_PATTERN, MD_TEE_PATTERN]) {
|
|
246
244
|
const matcher = new RegExp(pattern.source, pattern.flags);
|
|
247
|
-
for (const match of
|
|
245
|
+
for (const match of commandText.matchAll(matcher)) {
|
|
248
246
|
const target = match[1].trim().replace(/^["']+|["']+$/g, "");
|
|
249
247
|
if (target) targets.push(target);
|
|
250
248
|
}
|
|
@@ -252,6 +250,53 @@ export function commandMatches(command: string): string[] {
|
|
|
252
250
|
return targets;
|
|
253
251
|
}
|
|
254
252
|
|
|
253
|
+
/** Claude tool payloads in the shape the shared secret guard reads. */
|
|
254
|
+
function secretToolCall(
|
|
255
|
+
toolName: string,
|
|
256
|
+
toolInput: Record<string, unknown>,
|
|
257
|
+
ctx: GuardContext,
|
|
258
|
+
): SecretToolCall | null {
|
|
259
|
+
const text = (value: unknown) => (typeof value === "string" ? value : "");
|
|
260
|
+
const target = (value: unknown) =>
|
|
261
|
+
text(value) ? resolveArtifactTarget(text(value), ctx.repoRoot) : "";
|
|
262
|
+
switch (toolName) {
|
|
263
|
+
case "Read":
|
|
264
|
+
return { kind: "read", paths: [target(toolInput.file_path)] };
|
|
265
|
+
case "Write":
|
|
266
|
+
return {
|
|
267
|
+
kind: "write",
|
|
268
|
+
writes: [{ path: target(toolInput.file_path), content: text(toolInput.content) }],
|
|
269
|
+
};
|
|
270
|
+
case "Edit":
|
|
271
|
+
return {
|
|
272
|
+
kind: "write",
|
|
273
|
+
writes: [{ path: target(toolInput.file_path), content: text(toolInput.new_string) }],
|
|
274
|
+
};
|
|
275
|
+
case "MultiEdit": {
|
|
276
|
+
const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
|
|
277
|
+
const content = edits
|
|
278
|
+
.map((edit) => (edit && typeof edit === "object" ? text(edit.new_string) : ""))
|
|
279
|
+
.join("\n");
|
|
280
|
+
return { kind: "write", writes: [{ path: target(toolInput.file_path), content }] };
|
|
281
|
+
}
|
|
282
|
+
case "NotebookEdit":
|
|
283
|
+
return {
|
|
284
|
+
kind: "write",
|
|
285
|
+
writes: [{ path: target(toolInput.notebook_path), content: text(toolInput.new_source) }],
|
|
286
|
+
};
|
|
287
|
+
case "Bash":
|
|
288
|
+
return { kind: "shell", command: text(toolInput.command) };
|
|
289
|
+
case "Grep":
|
|
290
|
+
return {
|
|
291
|
+
kind: "search",
|
|
292
|
+
paths: [target(toolInput.path)],
|
|
293
|
+
globs: [text(toolInput.glob)],
|
|
294
|
+
};
|
|
295
|
+
default:
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
/**
|
|
256
301
|
* Fails open only when no Mate context resolves from either source; in a
|
|
257
302
|
* wrapped Working Repository the guard runs without a launch.
|
|
@@ -273,6 +318,16 @@ export async function evaluate(
|
|
|
273
318
|
? (input.tool_input as Record<string, unknown>)
|
|
274
319
|
: {};
|
|
275
320
|
|
|
321
|
+
/** Secrets first: a secret refusal wins over every artifact-placement verdict. */
|
|
322
|
+
const secretCall = secretToolCall(toolName, toolInput, ctx);
|
|
323
|
+
if (secretCall) {
|
|
324
|
+
const refusal = await checkSecretAccess(secretCall, {
|
|
325
|
+
companionPath: ctx.companion,
|
|
326
|
+
isCompanionPath: (target) => isCompanionPath(target, ctx),
|
|
327
|
+
});
|
|
328
|
+
if (refusal) return { exitCode: 2, stderr: `${refusal}\n` };
|
|
329
|
+
}
|
|
330
|
+
|
|
276
331
|
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
277
332
|
const filePath = String(toolInput.file_path ?? "");
|
|
278
333
|
/** The path refusal wins when both apply: its message is not replaced. */
|
|
@@ -282,6 +337,9 @@ export async function evaluate(
|
|
|
282
337
|
|
|
283
338
|
if (toolName === "Bash") {
|
|
284
339
|
const command = String(toolInput.command ?? "");
|
|
340
|
+
const attribution = checkCommitAttribution(command);
|
|
341
|
+
if (attribution) return { exitCode: 2, stderr: `${attribution}\n` };
|
|
342
|
+
|
|
285
343
|
const targets = commandMatches(command);
|
|
286
344
|
for (const target of targets) {
|
|
287
345
|
const outcome = checkFilePath(target, ctx);
|
package/src/index.ts
CHANGED
|
@@ -39,13 +39,17 @@ export type {
|
|
|
39
39
|
McpService,
|
|
40
40
|
Plugin,
|
|
41
41
|
PluginCliCommand,
|
|
42
|
+
PluginDirectoryContribution,
|
|
42
43
|
PluginPolicy,
|
|
43
44
|
PluginRegistration,
|
|
44
45
|
ProviderHosting,
|
|
45
46
|
ProviderPlugin,
|
|
47
|
+
RuntimeContributions,
|
|
48
|
+
RuntimeContributionsByRuntime,
|
|
46
49
|
SetupContext,
|
|
47
50
|
TemplatesService,
|
|
48
51
|
} from "./tools/setup/plugin";
|
|
52
|
+
export type { SessionKind } from "./runtime/session-kind";
|
|
49
53
|
export type {
|
|
50
54
|
FrameworkType,
|
|
51
55
|
FrameworkConfig,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { pathIsDirectory } from "./fs-utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where a change lives in a Companion Repository: the active `openspec/changes/<name>/`
|
|
8
|
+
* directory, its dated archive `openspec/changes/archive/YYYY-MM-DD-<name>/`, and the
|
|
9
|
+
* canonical specs its delta specs map to. Only directories count, and only names are read.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Companion-relative POSIX paths. */
|
|
13
|
+
export const CHANGES_RELATIVE_DIR = "openspec/changes";
|
|
14
|
+
export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
|
|
15
|
+
export const SPECS_RELATIVE_DIR = "openspec/specs";
|
|
16
|
+
|
|
17
|
+
const ARCHIVE_DIR_NAME = "archive";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One path segment: no separator, NUL, or leading dot (so never `.`, `..`, or hidden).
|
|
21
|
+
* Wider than OpenSpec's kebab-case, so archives made under older rules still resolve.
|
|
22
|
+
*/
|
|
23
|
+
const SEGMENT = /^[^./\\\0][^/\\\0]*$/;
|
|
24
|
+
|
|
25
|
+
/** The `YYYY-MM-DD-<name>` anchor `openspec archive` gives an archived change. */
|
|
26
|
+
const ANCHOR = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
|
|
27
|
+
|
|
28
|
+
export function changesDirOf(companionPath: string): string {
|
|
29
|
+
return path.join(companionPath, ...CHANGES_RELATIVE_DIR.split("/"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function archiveDirOf(companionPath: string): string {
|
|
33
|
+
return path.join(companionPath, ...ARCHIVE_RELATIVE_DIR.split("/"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A change name or archive anchor: one path segment that is not the archive directory. */
|
|
37
|
+
export function isChangeName(candidate: string): boolean {
|
|
38
|
+
return SEGMENT.test(candidate) && candidate !== ARCHIVE_DIR_NAME;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Anchor {
|
|
42
|
+
date: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `2026-01-02-acme` → `{date: "2026-01-02", name: "acme"}`; null for anything else, `2026-01-02-` included. */
|
|
47
|
+
export function parseAnchor(candidate: string): Anchor | null {
|
|
48
|
+
const match = ANCHOR.exec(candidate);
|
|
49
|
+
if (!match || !isChangeName(match[2])) return null;
|
|
50
|
+
return { date: match[1], name: match[2] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The change name of an archive anchor (`2026-01-02-acme` → `acme`); other names pass through. */
|
|
54
|
+
export function changeNameOf(anchorOrName: string): string {
|
|
55
|
+
return parseAnchor(anchorOrName)?.name ?? anchorOrName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function directoryNames(dir: string): Promise<string[]> {
|
|
59
|
+
try {
|
|
60
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
61
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Active change names, sorted. */
|
|
68
|
+
export async function activeChanges(companionPath: string): Promise<string[]> {
|
|
69
|
+
return (await directoryNames(changesDirOf(companionPath))).filter(isChangeName).toSorted();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Archive anchors, optionally only those of change `name`, sorted oldest first. */
|
|
73
|
+
export async function archiveAnchors(companionPath: string, name?: string): Promise<string[]> {
|
|
74
|
+
return (await directoryNames(archiveDirOf(companionPath)))
|
|
75
|
+
.filter((anchor) => {
|
|
76
|
+
const parsed = parseAnchor(anchor);
|
|
77
|
+
return parsed !== null && (name === undefined || parsed.name === name);
|
|
78
|
+
})
|
|
79
|
+
.toSorted();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ChangeLocation {
|
|
83
|
+
dir: string;
|
|
84
|
+
archived: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a change name or anchor to its directory: the archive an anchor names exactly,
|
|
89
|
+
* else the active change, else the newest archive of that name. Anything that is not a
|
|
90
|
+
* single change-name segment resolves to nothing.
|
|
91
|
+
*/
|
|
92
|
+
export async function locateChange(
|
|
93
|
+
companionPath: string,
|
|
94
|
+
target: string,
|
|
95
|
+
): Promise<ChangeLocation | null> {
|
|
96
|
+
if (!isChangeName(target)) return null;
|
|
97
|
+
const archiveDir = archiveDirOf(companionPath);
|
|
98
|
+
if (parseAnchor(target) && (await pathIsDirectory(path.join(archiveDir, target)))) {
|
|
99
|
+
return { dir: path.join(archiveDir, target), archived: true };
|
|
100
|
+
}
|
|
101
|
+
const active = path.join(changesDirOf(companionPath), target);
|
|
102
|
+
if (await pathIsDirectory(active)) return { dir: active, archived: false };
|
|
103
|
+
const newest = (await archiveAnchors(companionPath, target)).at(-1);
|
|
104
|
+
return newest ? { dir: path.join(archiveDir, newest), archived: true } : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DeltaSpec {
|
|
108
|
+
/** POSIX path under the change's `specs/`, e.g. `identity/user-auth/spec.md`. */
|
|
109
|
+
file: string;
|
|
110
|
+
/** The companion-relative canonical path the file maps to. */
|
|
111
|
+
canonical: string;
|
|
112
|
+
/** The capability path of a `spec.md` below `specs/`; null for any other file. */
|
|
113
|
+
capability: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Every file below `dir` at any depth, as sorted POSIX paths relative to it. Reads directory
|
|
118
|
+
* entry names only, never contents; an unreadable directory contributes nothing.
|
|
119
|
+
*/
|
|
120
|
+
export async function filesBelow(dir: string): Promise<string[]> {
|
|
121
|
+
const walk = async (current: string, relative: string): Promise<string[]> => {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const nested = await Promise.all(
|
|
129
|
+
entries.map(async (entry) => {
|
|
130
|
+
const entryRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
131
|
+
if (entry.isDirectory()) return walk(path.join(current, entry.name), entryRelative);
|
|
132
|
+
return entry.isFile() ? [entryRelative] : [];
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
return nested.flat();
|
|
136
|
+
};
|
|
137
|
+
return (await walk(dir, "")).toSorted();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every file under the change's `specs/`, at any depth, sorted. Reads directory entry
|
|
142
|
+
* names only, never contents. A change without `specs/` has none.
|
|
143
|
+
*/
|
|
144
|
+
export async function deltaSpecs(changeDir: string): Promise<DeltaSpec[]> {
|
|
145
|
+
const files = await filesBelow(path.join(changeDir, "specs"));
|
|
146
|
+
return files.map((file) => {
|
|
147
|
+
const capability = path.posix.dirname(file);
|
|
148
|
+
return {
|
|
149
|
+
file,
|
|
150
|
+
canonical: `${SPECS_RELATIVE_DIR}/${file}`,
|
|
151
|
+
capability: path.posix.basename(file) === "spec.md" && capability !== "." ? capability : null,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
|
|
3
|
+
import { BRAND_ACCENT } from "../../brand/palette";
|
|
4
|
+
|
|
3
5
|
interface WizardHeaderProps {
|
|
4
6
|
title: string;
|
|
5
7
|
step: number;
|
|
@@ -12,7 +14,7 @@ export function WizardHeader({ title, step, totalSteps }: WizardHeaderProps) {
|
|
|
12
14
|
return (
|
|
13
15
|
<Box borderStyle="round" width="100%">
|
|
14
16
|
<Box flexDirection="column" flexGrow={1} marginLeft={1}>
|
|
15
|
-
<Text bold color=
|
|
17
|
+
<Text bold color={BRAND_ACCENT}>
|
|
16
18
|
mate framework
|
|
17
19
|
</Text>
|
|
18
20
|
<Box justifyContent="space-between">
|
|
@@ -54,9 +54,17 @@ function validateContextModeNodeRuntime(): void {
|
|
|
54
54
|
export async function validateContextModePackage(companionPath: string): Promise<void> {
|
|
55
55
|
validateContextModeNodeRuntime();
|
|
56
56
|
const root = getContextModePackageRoot(companionPath);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let manifestText: string;
|
|
58
|
+
try {
|
|
59
|
+
manifestText = await fs.readFile(path.join(root, "package.json"), "utf8");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Expected ${getContextModePackageReference()} at ${root}; it is not installed. Re-run \`mate companion setup\`.`,
|
|
64
|
+
{ cause: error },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const manifest = JSON.parse(manifestText) as { version?: string };
|
|
60
68
|
if (manifest.version !== CONTEXT_MODE_VERSION) {
|
|
61
69
|
throw new Error(
|
|
62
70
|
`Expected ${getContextModePackageReference()} at ${root}; found ${manifest.version ?? "an unknown version"}. Re-run \`mate companion setup\`.`,
|
|
@@ -72,6 +80,15 @@ export async function validateContextModePackage(companionPath: string): Promise
|
|
|
72
80
|
);
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
export const contextModePackageDeps = {
|
|
84
|
+
runInstall: (cwd: string) =>
|
|
85
|
+
spawnSync(
|
|
86
|
+
"npm",
|
|
87
|
+
["install", "--no-audit", "--no-fund", "--loglevel=error", "--registry", PUBLIC_NPM_REGISTRY],
|
|
88
|
+
{ cwd, encoding: "utf8" as const, env: process.env },
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
75
92
|
export async function installContextModePackage(companionPath: string): Promise<void> {
|
|
76
93
|
validateContextModeNodeRuntime();
|
|
77
94
|
const installDir = getContextModeInstallDir(companionPath);
|
|
@@ -85,15 +102,11 @@ export async function installContextModePackage(companionPath: string): Promise<
|
|
|
85
102
|
) + "\n",
|
|
86
103
|
"utf8",
|
|
87
104
|
);
|
|
88
|
-
const result =
|
|
89
|
-
"npm",
|
|
90
|
-
["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
|
|
91
|
-
{ cwd: installDir, encoding: "utf8", env: process.env },
|
|
92
|
-
);
|
|
105
|
+
const result = contextModePackageDeps.runInstall(installDir);
|
|
93
106
|
if (result.error || result.status !== 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
107
|
+
const cause =
|
|
108
|
+
result.error?.message ?? (result.stderr?.trim() || `npm exited with ${result.status}`);
|
|
109
|
+
throw new Error(`Could not install ${getContextModePackageReference()}: ${cause}`);
|
|
97
110
|
}
|
|
98
111
|
await validateContextModePackage(companionPath);
|
|
99
112
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { locateChange } from "../change-layout";
|
|
2
|
+
import { runOpenSpec, type RunOpenSpec } from "../openspec-cli";
|
|
3
|
+
import { companionRelative, type DeliveryEnvironment } from "./environment";
|
|
4
|
+
import { archiveWarnings, planningWarning, type ArchiveWarning } from "./overlap";
|
|
5
|
+
import {
|
|
6
|
+
isPublishable,
|
|
7
|
+
settleDelivery,
|
|
8
|
+
type DeliveryRecord,
|
|
9
|
+
type DeliverySettlement,
|
|
10
|
+
type UnscopedDelivery,
|
|
11
|
+
} from "./record";
|
|
12
|
+
import {
|
|
13
|
+
recordDelivery,
|
|
14
|
+
type DeliveryEdit,
|
|
15
|
+
type DeliveryRefusal,
|
|
16
|
+
type RecordDeliveryDeps,
|
|
17
|
+
} from "./record-delivery";
|
|
18
|
+
import type { BranchOutcome } from "./refresh";
|
|
19
|
+
|
|
20
|
+
export type ArchiveStatus = "archived" | "needs-branch" | "failed";
|
|
21
|
+
|
|
22
|
+
export interface ArchiveResult {
|
|
23
|
+
status: ArchiveStatus;
|
|
24
|
+
change: string;
|
|
25
|
+
/** Companion-relative archive directory, once archived. */
|
|
26
|
+
path: string | null;
|
|
27
|
+
record: DeliveryRecord;
|
|
28
|
+
settlement: DeliverySettlement;
|
|
29
|
+
outcomes: BranchOutcome[];
|
|
30
|
+
warnings: ArchiveWarning[];
|
|
31
|
+
/** `needs-branch` only: the branch to suggest, when it exists. */
|
|
32
|
+
suggestedBranch?: string | null;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ArchiveRequest {
|
|
37
|
+
change: string;
|
|
38
|
+
/** The repository `delivery` is recorded for; defaults to the Working Repository. */
|
|
39
|
+
repository?: string;
|
|
40
|
+
/** Recorded before the refresh, as `artifact link` would. */
|
|
41
|
+
delivery?: DeliveryEdit["delivery"];
|
|
42
|
+
/** Archive with an unlinked record instead of answering `needs-branch`. */
|
|
43
|
+
skipBranch?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ArchiveOutcome = { refusal: DeliveryRefusal } | { result: ArchiveResult };
|
|
47
|
+
|
|
48
|
+
export interface ArchiveChangeDeps extends RecordDeliveryDeps {
|
|
49
|
+
runOpenSpec?: RunOpenSpec;
|
|
50
|
+
/**
|
|
51
|
+
* Called once before `openspec archive` starts, so warnings are visible even when the
|
|
52
|
+
* archive fails or hangs. Never called for `needs-branch` or a refusal.
|
|
53
|
+
*/
|
|
54
|
+
onWarnings?: (warnings: ArchiveWarning[], unscoped: UnscopedDelivery[]) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Archive change stage: records and refreshes the delivery, collects overlap,
|
|
59
|
+
* dependency, and incomplete-planning warnings, then runs `openspec archive`. Without a
|
|
60
|
+
* recorded delivery and no `skipBranch`, nothing is archived and the result is
|
|
61
|
+
* `needs-branch`. Merge state is reported, never a refusal.
|
|
62
|
+
*/
|
|
63
|
+
export async function archiveChange(
|
|
64
|
+
environment: DeliveryEnvironment,
|
|
65
|
+
request: ArchiveRequest,
|
|
66
|
+
deps: ArchiveChangeDeps = {},
|
|
67
|
+
): Promise<ArchiveOutcome> {
|
|
68
|
+
const { change } = request;
|
|
69
|
+
const outcome = await recordDelivery(
|
|
70
|
+
environment,
|
|
71
|
+
{
|
|
72
|
+
changes: [change],
|
|
73
|
+
...(request.repository ? { repository: request.repository } : {}),
|
|
74
|
+
...(request.delivery ? { edit: { delivery: request.delivery } } : {}),
|
|
75
|
+
refresh: true,
|
|
76
|
+
// The settlement covers every repository, so each delivery's merge state is refreshed.
|
|
77
|
+
refreshAll: true,
|
|
78
|
+
requireDelivery: !request.skipBranch,
|
|
79
|
+
},
|
|
80
|
+
deps,
|
|
81
|
+
);
|
|
82
|
+
if ("refusal" in outcome) return outcome;
|
|
83
|
+
|
|
84
|
+
const runner = deps.runOpenSpec ?? runOpenSpec;
|
|
85
|
+
const cwd = environment.companionPath;
|
|
86
|
+
const [overlap, status] = await Promise.all([
|
|
87
|
+
archiveWarnings(environment.companionPath, change),
|
|
88
|
+
runner(["status", "--change", change, "--json"], { cwd }),
|
|
89
|
+
]);
|
|
90
|
+
const planning = status.code === 0 ? planningWarning(change, status.stdout) : null;
|
|
91
|
+
const warnings = [...overlap, ...(planning ? [planning] : [])];
|
|
92
|
+
|
|
93
|
+
if ("needsBranch" in outcome) {
|
|
94
|
+
const { record, suggestedBranch } = outcome.needsBranch;
|
|
95
|
+
return {
|
|
96
|
+
result: {
|
|
97
|
+
status: "needs-branch",
|
|
98
|
+
change,
|
|
99
|
+
path: null,
|
|
100
|
+
record,
|
|
101
|
+
settlement: settleDelivery(record),
|
|
102
|
+
outcomes: [],
|
|
103
|
+
warnings,
|
|
104
|
+
suggestedBranch,
|
|
105
|
+
message: `No delivery branch is recorded for ${change}. Re-run with --branch <branch>, --not-applicable <reason>, or --skip-branch.`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [{ record, settlement, outcomes }] = outcome.results;
|
|
111
|
+
deps.onWarnings?.(warnings, settlement.unscoped);
|
|
112
|
+
|
|
113
|
+
const run = await runner(["archive", change, "--yes"], { cwd });
|
|
114
|
+
if (run.code !== 0) {
|
|
115
|
+
const detail = (run.stderr.trim() || run.stdout.trim()).split("\n").slice(-5).join("\n");
|
|
116
|
+
return {
|
|
117
|
+
result: {
|
|
118
|
+
status: "failed",
|
|
119
|
+
change,
|
|
120
|
+
path: null,
|
|
121
|
+
record,
|
|
122
|
+
settlement,
|
|
123
|
+
outcomes,
|
|
124
|
+
warnings,
|
|
125
|
+
message: `openspec archive ${change} failed (exit ${run.code ?? "unknown"}): ${detail}`,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const archived = await locateChange(environment.companionPath, change);
|
|
131
|
+
return {
|
|
132
|
+
result: {
|
|
133
|
+
status: "archived",
|
|
134
|
+
change,
|
|
135
|
+
path: archived?.archived ? companionRelative(environment, archived.dir) : null,
|
|
136
|
+
record,
|
|
137
|
+
settlement,
|
|
138
|
+
outcomes,
|
|
139
|
+
warnings,
|
|
140
|
+
message: archivedMessage(change, settlement),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function archivedMessage(change: string, settlement: DeliverySettlement): string {
|
|
146
|
+
if (isPublishable(settlement.status)) {
|
|
147
|
+
return `Archived ${change}; it can be published with mate-publish-artifact.`;
|
|
148
|
+
}
|
|
149
|
+
const missing =
|
|
150
|
+
settlement.missing.length > 0
|
|
151
|
+
? ` No delivery is recorded for ${settlement.missing.join(", ")}, which its scopes name.`
|
|
152
|
+
: "";
|
|
153
|
+
return `Archived ${change}; publication waits until its delivery is merged (${settlement.status}).${missing}`;
|
|
154
|
+
}
|