@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DeliveryEnvironment } from "../../../../lib/delivery/environment";
|
|
2
|
+
import { sameRepository } from "../../../../lib/delivery/record";
|
|
3
|
+
import type { DeliveryEdit } from "../../../../lib/delivery/record-delivery";
|
|
4
|
+
import { type CliFlags, malformedValueFlag, stringFlag } from "../../../parse-flags";
|
|
5
|
+
|
|
6
|
+
/** `--branch` with `--direct`, or `--not-applicable`, as `artifact link` and `archive` take them. */
|
|
7
|
+
export interface BranchFlags {
|
|
8
|
+
branch?: string;
|
|
9
|
+
direct: boolean;
|
|
10
|
+
notApplicable?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function branchFlagsFrom(flags: CliFlags): BranchFlags {
|
|
14
|
+
return {
|
|
15
|
+
branch: stringFlag(flags.branch),
|
|
16
|
+
direct: flags.direct === true,
|
|
17
|
+
notApplicable: stringFlag(flags["not-applicable"]),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DIRECT_NEEDS_BRANCH = "mate: --direct records a direct delivery and needs --branch.";
|
|
22
|
+
|
|
23
|
+
/** The delivery edit the branch flags ask for; `--branch` wins over `--not-applicable`. */
|
|
24
|
+
export function deliveryEditFrom(flags: BranchFlags): DeliveryEdit["delivery"] {
|
|
25
|
+
if (flags.branch !== undefined) return { branch: flags.branch, direct: flags.direct };
|
|
26
|
+
if (flags.notApplicable !== undefined) return { notApplicable: flags.notApplicable };
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Refuses a value flag given bare or repeated, which would otherwise read as absent. */
|
|
31
|
+
export function valueFlagError(flags: CliFlags, names: readonly string[]): string | undefined {
|
|
32
|
+
const name = malformedValueFlag(flags, names);
|
|
33
|
+
return name === undefined ? undefined : `mate: --${name} takes exactly one value.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `--repository <org/repo>`, or the refusal for a malformed one. */
|
|
37
|
+
export function repositoryFlag(flags: CliFlags): { repository?: string } | { error: string } {
|
|
38
|
+
const repository = stringFlag(flags.repository);
|
|
39
|
+
if (repository !== undefined && !/^[\w.-]+(?:\/[\w.-]+)+$/.test(repository)) {
|
|
40
|
+
return {
|
|
41
|
+
error:
|
|
42
|
+
"mate: --repository expects a repository path such as <org/repo> or <group/subgroup/repo>.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return repository === undefined ? {} : { repository };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** GitHub slugs are case-insensitive; the Working Repository's own spelling keeps one delivery. */
|
|
49
|
+
export function canonicalRepository(
|
|
50
|
+
environment: DeliveryEnvironment,
|
|
51
|
+
requested: string | undefined,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
const own = environment.workingRepo?.slug;
|
|
54
|
+
return own && requested !== undefined && sameRepository(requested, own) ? own : requested;
|
|
55
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
workingRepoGit,
|
|
3
|
+
configuredProtectedPatterns,
|
|
4
|
+
type WorkingRepoGit,
|
|
5
|
+
} from "../../../../lib/delivery/branches";
|
|
6
|
+
import type { DeliveryEnvironment, WorkingRepoInfo } from "../../../../lib/delivery/environment";
|
|
7
|
+
import { parseRemote, repositoryPath, type ForgeRepo } from "../../../../lib/delivery/forge";
|
|
8
|
+
import { sameRepository } from "../../../../lib/delivery/record";
|
|
9
|
+
import { hasOpenspecCapability } from "../../../../lib/orchestrator/capabilities";
|
|
10
|
+
import {
|
|
11
|
+
resolveForCapability,
|
|
12
|
+
type LaunchContext,
|
|
13
|
+
} from "../../../../lib/orchestrator/framework-context";
|
|
14
|
+
import type { CapabilityConfig, LinkedRepository } from "../../../../lib/orchestrator/types";
|
|
15
|
+
import { WorkingRepoRequiredError } from "../../../../lib/orchestrator/types";
|
|
16
|
+
import { ensureUnambiguousCompanion } from "../../shared/companion-selection";
|
|
17
|
+
|
|
18
|
+
export interface DeliveryEnvironmentDeps {
|
|
19
|
+
ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
|
|
20
|
+
resolveContext?: (cwd: string) => Promise<LaunchContext>;
|
|
21
|
+
loadCapabilities?: (context: LaunchContext) => Promise<CapabilityConfig[]>;
|
|
22
|
+
workingRepoGit?: (repoPath: string) => WorkingRepoGit;
|
|
23
|
+
loadLinkedRepositories?: (context: LaunchContext) => Promise<LinkedRepository[]>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function defaultLoadLinkedRepositories(context: LaunchContext): Promise<LinkedRepository[]> {
|
|
27
|
+
return (await context.workingRepoStore.load()).repos;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function defaultLoadCapabilities(context: LaunchContext): Promise<CapabilityConfig[]> {
|
|
31
|
+
const config = await context.configStore.load();
|
|
32
|
+
return config.capabilities ?? [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The companion, the Working Repository with its remote identity, and the configured
|
|
37
|
+
* protected-branch patterns. `error` explains why the command cannot run; it is null
|
|
38
|
+
* when the companion check has already reported the problem.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveDeliveryEnvironment(
|
|
41
|
+
deps: DeliveryEnvironmentDeps,
|
|
42
|
+
): Promise<DeliveryEnvironment | { error: string | null }> {
|
|
43
|
+
const ensureCompanion = deps.ensureUnambiguousCompanion ?? ensureUnambiguousCompanion;
|
|
44
|
+
if (!(await ensureCompanion(process.cwd()))) return { error: null };
|
|
45
|
+
|
|
46
|
+
let context: LaunchContext;
|
|
47
|
+
try {
|
|
48
|
+
context = await (deps.resolveContext ?? ((cwd: string) => resolveForCapability(cwd)))(
|
|
49
|
+
process.cwd(),
|
|
50
|
+
);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err instanceof WorkingRepoRequiredError) return { error: err.message };
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const capabilities = await (deps.loadCapabilities ?? defaultLoadCapabilities)(context);
|
|
57
|
+
if (!hasOpenspecCapability(capabilities)) {
|
|
58
|
+
return { error: "mate: the openspec capability must be enabled to record delivery." };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const openGit = deps.workingRepoGit ?? workingRepoGit;
|
|
62
|
+
const repoPath = context.repository?.path ?? process.env.MATE_REPO_PATH;
|
|
63
|
+
let workingRepo: WorkingRepoInfo | null = null;
|
|
64
|
+
if (repoPath) {
|
|
65
|
+
const git = openGit(repoPath);
|
|
66
|
+
const remote = await git.remoteUrl();
|
|
67
|
+
const forgeRepo = remote ? parseRemote(remote) : null;
|
|
68
|
+
workingRepo = {
|
|
69
|
+
path: repoPath,
|
|
70
|
+
slug: forgeRepo ? repositoryPath(forgeRepo) : null,
|
|
71
|
+
forgeRepo,
|
|
72
|
+
git,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let linkedRemotes: Promise<ForgeRepo[]> | undefined;
|
|
77
|
+
// Read once, and only when a repository other than the Working Repository needs a host.
|
|
78
|
+
const linked = (): Promise<ForgeRepo[]> =>
|
|
79
|
+
(linkedRemotes ??= (async () => {
|
|
80
|
+
// An unreadable registry only loses host hints; the Working Repository's host remains.
|
|
81
|
+
const repositories = await (deps.loadLinkedRepositories ?? defaultLoadLinkedRepositories)(
|
|
82
|
+
context,
|
|
83
|
+
).catch(() => []);
|
|
84
|
+
const remotes = await Promise.all(
|
|
85
|
+
repositories
|
|
86
|
+
.filter((repository) => repository.path !== repoPath)
|
|
87
|
+
.map((repository) => openGit(repository.path).remoteUrl()),
|
|
88
|
+
);
|
|
89
|
+
return remotes.flatMap((remote) => (remote ? (parseRemote(remote) ?? []) : []));
|
|
90
|
+
})());
|
|
91
|
+
const hostOf = async (repository: string): Promise<string | null> => {
|
|
92
|
+
const match = (await linked()).find((remote) =>
|
|
93
|
+
sameRepository(repositoryPath(remote), repository),
|
|
94
|
+
);
|
|
95
|
+
return match?.host ?? workingRepo?.forgeRepo?.host ?? null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
companionPath: context.companionPath,
|
|
100
|
+
workingRepo,
|
|
101
|
+
protectedPatterns: configuredProtectedPatterns(capabilities),
|
|
102
|
+
hostOf,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordDelivery,
|
|
3
|
+
type DeliveryRefusal,
|
|
4
|
+
type DeliveryResult,
|
|
5
|
+
type RecordDeliveryDeps,
|
|
6
|
+
} from "../../../../lib/delivery/record-delivery";
|
|
7
|
+
import { applyWarnings, type DependencyWarning } from "../../../../lib/delivery/overlap";
|
|
8
|
+
import type { MergeState } from "../../../../lib/delivery/record";
|
|
9
|
+
import { type BooleanFlagSet, parseFlags, positionalArgs, stringFlag } from "../../../parse-flags";
|
|
10
|
+
import {
|
|
11
|
+
branchFlagsFrom,
|
|
12
|
+
canonicalRepository,
|
|
13
|
+
deliveryEditFrom,
|
|
14
|
+
DIRECT_NEEDS_BRANCH,
|
|
15
|
+
repositoryFlag,
|
|
16
|
+
valueFlagError,
|
|
17
|
+
} from "./branch-flags";
|
|
18
|
+
import { resolveDeliveryEnvironment, type DeliveryEnvironmentDeps } from "./environment";
|
|
19
|
+
import {
|
|
20
|
+
commandOutput,
|
|
21
|
+
renderDelivery,
|
|
22
|
+
sharedRefusalMessage,
|
|
23
|
+
unscopedDeliveryWarning,
|
|
24
|
+
} from "./output";
|
|
25
|
+
|
|
26
|
+
export interface LinkCommandDeps extends DeliveryEnvironmentDeps, RecordDeliveryDeps {
|
|
27
|
+
stdout?: (line: string) => void;
|
|
28
|
+
stderr?: (line: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BOOLEAN_FLAGS: BooleanFlagSet = new Set([
|
|
32
|
+
"json",
|
|
33
|
+
"refresh",
|
|
34
|
+
"direct",
|
|
35
|
+
"confirm-merged",
|
|
36
|
+
"confirm-open",
|
|
37
|
+
"confirm-dropped",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const CONFIRMATIONS: Record<string, MergeState> = {
|
|
41
|
+
"confirm-merged": "merged",
|
|
42
|
+
"confirm-open": "open",
|
|
43
|
+
"confirm-dropped": "closed-unmerged",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** `warnings`: declared dependencies not implemented yet; they inform and never block. */
|
|
47
|
+
export type LinkResult = DeliveryResult & { warnings: DependencyWarning[] };
|
|
48
|
+
|
|
49
|
+
function refusalMessage(refusal: DeliveryRefusal): string {
|
|
50
|
+
if (refusal.reason === "unknown-change") {
|
|
51
|
+
return `mate: no active or archived change named "${refusal.change}".`;
|
|
52
|
+
}
|
|
53
|
+
return sharedRefusalMessage(refusal);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderHuman(result: LinkResult): string[] {
|
|
57
|
+
const lines = [`${result.change}: ${result.settlement.status}`];
|
|
58
|
+
lines.push(...result.record.deliveries.map(renderDelivery));
|
|
59
|
+
for (const repository of result.settlement.missing) {
|
|
60
|
+
lines.push(` ${repository}: no delivery recorded (named in the change's scopes)`);
|
|
61
|
+
}
|
|
62
|
+
for (const outcome of result.outcomes.filter((entry) => entry.needsAnswer)) {
|
|
63
|
+
lines.push(` ${outcome.branch}: needs an answer (${outcome.answerReason})`);
|
|
64
|
+
if (outcome.failure) lines.push(` ${outcome.failure.message}`);
|
|
65
|
+
for (const candidate of outcome.candidates ?? []) {
|
|
66
|
+
lines.push(` #${candidate.number} ${candidate.state} ${candidate.url}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @command mate artifact link <change>...
|
|
74
|
+
* @description Writes a change's Delivery Record (`delivery.yaml`), with `artifact archive`
|
|
75
|
+
* the only writer of that file, for active and archived changes. Records the delivery branch or a
|
|
76
|
+
* not-applicable reason per Working Repository, an informational PR URL, merge results
|
|
77
|
+
* from the forge (`--refresh`), or the user's answer (`--confirm-*`, source `user`).
|
|
78
|
+
* @flags
|
|
79
|
+
* - `--repository <org/repo>` — the delivery to edit; defaults to the Working Repository.
|
|
80
|
+
* - `--branch <branch>` — record the delivery branch; a protected branch needs `--direct`.
|
|
81
|
+
* - `--direct` — with `--branch`, record a direct delivery, settled only by the user's answer; required for a protected branch.
|
|
82
|
+
* - `--not-applicable <reason>` — no branch delivers this change in the repository.
|
|
83
|
+
* - `--pr <url>` — record the PR URL; with `--refresh`, picks that PR among several.
|
|
84
|
+
* - `--refresh` — ask the forge for the merge state of each recorded branch; a direct delivery
|
|
85
|
+
* is reported as needing the user's answer.
|
|
86
|
+
* - `--confirm-merged` / `--confirm-open` / `--confirm-dropped` — the user's answer.
|
|
87
|
+
* - `--json` — emit an array of {@link LinkResult}, one per change.
|
|
88
|
+
* @remarks Warns, without blocking, about each `dependsOn` change that is neither archived nor
|
|
89
|
+
* fully implemented, since apply records the branch before writing code. Without an edit flag or `--refresh` it only reads the record and writes nothing.
|
|
90
|
+
* Network requests go only to the forge API: with `--refresh`, and with `--branch` for
|
|
91
|
+
* another repository's default branch. The forge token is never
|
|
92
|
+
* printed, logged, or written.
|
|
93
|
+
*/
|
|
94
|
+
export async function runArtifactLinkCommand(
|
|
95
|
+
argv: string[],
|
|
96
|
+
deps: LinkCommandDeps = {},
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const { emitOut, emitErr, refuse } = commandOutput(deps);
|
|
99
|
+
const flags = parseFlags(argv, BOOLEAN_FLAGS);
|
|
100
|
+
const flagError = valueFlagError(flags, ["repository", "branch", "not-applicable", "pr"]);
|
|
101
|
+
if (flagError) return refuse(flagError);
|
|
102
|
+
const changes = positionalArgs(argv, BOOLEAN_FLAGS);
|
|
103
|
+
const branchFlags = branchFlagsFrom(flags);
|
|
104
|
+
const { branch, notApplicable } = branchFlags;
|
|
105
|
+
const pr = stringFlag(flags.pr);
|
|
106
|
+
const confirmations = Object.keys(CONFIRMATIONS).filter((flag) => flags[flag] === true);
|
|
107
|
+
|
|
108
|
+
if (changes.length === 0) return refuse("mate: artifact link requires a change name.");
|
|
109
|
+
if (branch !== undefined && notApplicable !== undefined) {
|
|
110
|
+
return refuse("mate: --branch and --not-applicable exclude each other.");
|
|
111
|
+
}
|
|
112
|
+
if (confirmations.length > 1) {
|
|
113
|
+
return refuse("mate: pass one of --confirm-merged, --confirm-open, or --confirm-dropped.");
|
|
114
|
+
}
|
|
115
|
+
if (branchFlags.direct && branch === undefined) return refuse(DIRECT_NEEDS_BRANCH);
|
|
116
|
+
if (pr !== undefined && !/^https?:\/\/\S+$/.test(pr)) {
|
|
117
|
+
return refuse("mate: --pr expects the PR's URL.");
|
|
118
|
+
}
|
|
119
|
+
const requested = repositoryFlag(flags);
|
|
120
|
+
if ("error" in requested) return refuse(requested.error);
|
|
121
|
+
|
|
122
|
+
const environment = await resolveDeliveryEnvironment(deps);
|
|
123
|
+
if ("error" in environment) return refuse(environment.error);
|
|
124
|
+
const repository = canonicalRepository(environment, requested.repository);
|
|
125
|
+
const confirm = confirmations.length === 1 ? CONFIRMATIONS[confirmations[0]] : undefined;
|
|
126
|
+
const delivery = deliveryEditFrom(branchFlags);
|
|
127
|
+
const outcome = await recordDelivery(
|
|
128
|
+
environment,
|
|
129
|
+
{
|
|
130
|
+
changes,
|
|
131
|
+
...(repository ? { repository } : {}),
|
|
132
|
+
edit: {
|
|
133
|
+
...(delivery ? { delivery } : {}),
|
|
134
|
+
...(pr !== undefined ? { pr } : {}),
|
|
135
|
+
...(confirm ? { confirm } : {}),
|
|
136
|
+
},
|
|
137
|
+
refresh: flags.refresh === true,
|
|
138
|
+
includeArchived: true,
|
|
139
|
+
},
|
|
140
|
+
deps,
|
|
141
|
+
);
|
|
142
|
+
if ("refusal" in outcome) return refuse(refusalMessage(outcome.refusal));
|
|
143
|
+
const results: LinkResult[] = [];
|
|
144
|
+
for (const result of "results" in outcome ? outcome.results : []) {
|
|
145
|
+
const warnings = await applyWarnings(environment.companionPath, result.change);
|
|
146
|
+
for (const warning of warnings) emitErr(`warning: ${warning.message}`);
|
|
147
|
+
for (const entry of result.settlement.unscoped) {
|
|
148
|
+
emitErr(unscopedDeliveryWarning(result.change, entry));
|
|
149
|
+
}
|
|
150
|
+
results.push({ ...result, warnings });
|
|
151
|
+
}
|
|
152
|
+
if (flags.json === true) emitOut(JSON.stringify(results));
|
|
153
|
+
else for (const result of results) for (const line of renderHuman(result)) emitOut(line);
|
|
154
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Delivery, UnscopedDelivery } from "../../../../lib/delivery/record";
|
|
2
|
+
import type { DeliveryRefusal } from "../../../../lib/delivery/record-delivery";
|
|
3
|
+
|
|
4
|
+
export interface CommandOutput {
|
|
5
|
+
emitOut: (line: string) => void;
|
|
6
|
+
emitErr: (line: string) => void;
|
|
7
|
+
/** Writes `message`, when given, to stderr and sets a failing exit code. */
|
|
8
|
+
refuse: (message?: string | null) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function commandOutput(deps: {
|
|
12
|
+
stdout?: (line: string) => void;
|
|
13
|
+
stderr?: (line: string) => void;
|
|
14
|
+
}): CommandOutput {
|
|
15
|
+
const emitOut = deps.stdout ?? ((line: string) => process.stdout.write(`${line}\n`));
|
|
16
|
+
const emitErr = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
17
|
+
return {
|
|
18
|
+
emitOut,
|
|
19
|
+
emitErr,
|
|
20
|
+
refuse: (message) => {
|
|
21
|
+
if (message) emitErr(message);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One delivery as `<repository> <branch or reason> <merge> <pr>`. */
|
|
28
|
+
export function renderDelivery(delivery: Delivery): string {
|
|
29
|
+
if (delivery.notApplicable !== undefined) {
|
|
30
|
+
return ` ${delivery.repository} not applicable: ${delivery.notApplicable}`;
|
|
31
|
+
}
|
|
32
|
+
const branch = `${delivery.branch ?? "?"}${delivery.direct ? " [direct]" : ""}`;
|
|
33
|
+
const merge = delivery.merge
|
|
34
|
+
? `${delivery.merge.state} (${delivery.merge.source})`
|
|
35
|
+
: "no merge recorded";
|
|
36
|
+
const pr = delivery.pr ? ` ${delivery.pr}` : "";
|
|
37
|
+
return ` ${delivery.repository} ${branch} ${merge}${pr}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wording for the refusals `link` and `archive` share; `unknown-change` names each command. */
|
|
41
|
+
export function sharedRefusalMessage(
|
|
42
|
+
refusal: Exclude<DeliveryRefusal, { reason: "unknown-change" }>,
|
|
43
|
+
): string {
|
|
44
|
+
if (refusal.reason === "no-repository") {
|
|
45
|
+
return "mate: the Working Repository has no origin remote to identify it; pass --repository <org/repo>.";
|
|
46
|
+
}
|
|
47
|
+
if (refusal.reason === "protected-branch") {
|
|
48
|
+
return `mate: ${refusal.branch} is a protected branch (${refusal.protection}); record it only as an explicit direct delivery with --direct.`;
|
|
49
|
+
}
|
|
50
|
+
return refusal.for === "pr"
|
|
51
|
+
? `mate: ${refusal.change} has no delivery branch for ${refusal.repository}; pass --branch first.`
|
|
52
|
+
: `mate: ${refusal.change} has no delivery branch for ${refusal.repository} to confirm.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function unscopedDeliveryWarning(change: string, entry: UnscopedDelivery): string {
|
|
56
|
+
const hint = entry.closest ? `; did you mean ${entry.closest}?` : ".";
|
|
57
|
+
return `warning: ${entry.repository} matches none of ${change}'s scopes${hint}`;
|
|
58
|
+
}
|
|
@@ -8,8 +8,14 @@ import { discoverArchives, pendingArchives } from "../pending/discovery";
|
|
|
8
8
|
import { type FinishResult, runFinishEngine } from "./engine";
|
|
9
9
|
import type { ArtifactFinisher, FinishContext, FinisherFactory } from "./finisher";
|
|
10
10
|
import { defaultGitOps, type GitOps } from "./git";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
openspecSpecsFinisher,
|
|
13
|
+
publicationHold,
|
|
14
|
+
publicationHoldCache,
|
|
15
|
+
resolveDriftedSpecs,
|
|
16
|
+
} from "./openspec";
|
|
12
17
|
import { DEFAULT_FINISHER_TYPE, knownFinisherTypes, selectFinisher } from "./registry";
|
|
18
|
+
import { type PublicationSecretScan, scanPublication } from "./secret-scan";
|
|
13
19
|
|
|
14
20
|
export interface PublishCommandDeps {
|
|
15
21
|
ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
|
|
@@ -21,6 +27,11 @@ export interface PublishCommandDeps {
|
|
|
21
27
|
now?: () => Date;
|
|
22
28
|
/** Injectable spec-publication finisher; mirrors `selectFinisher` for the other unit. */
|
|
23
29
|
specsFinisher?: (context: FinishContext, paths: string[], now: () => Date) => ArtifactFinisher;
|
|
30
|
+
/** Injectable pre-commit secret scan over a publication's staged outputs. */
|
|
31
|
+
scanPublication?: (
|
|
32
|
+
companionPath: string,
|
|
33
|
+
commitPaths: string[],
|
|
34
|
+
) => Promise<PublicationSecretScan>;
|
|
24
35
|
stdout?: (line: string) => void;
|
|
25
36
|
stderr?: (line: string) => void;
|
|
26
37
|
}
|
|
@@ -99,7 +110,8 @@ function refusalResult(type: string, name: string, message: string): FinishResul
|
|
|
99
110
|
* new `openspec/specs/<date>-<specs>` tag. Bare, it publishes every drifted spec; with paths, only
|
|
100
111
|
* those. A bare name alongside it is a change target and is refused.
|
|
101
112
|
* - `--all` — publish every pending change in discovery order, then one spec publication for
|
|
102
|
-
* the drift that remains.
|
|
113
|
+
* the drift that remains. Skips an archive the merge gate holds back, reporting why, and
|
|
114
|
+
* halts at the first conflict or error; with `--json` it emits a
|
|
103
115
|
* single array of results in execution order.
|
|
104
116
|
* - `--type <type>` — finisher type to use; see `knownFinisherTypes()`. `--specs` and
|
|
105
117
|
* `--all` are openspec-only.
|
|
@@ -188,10 +200,14 @@ export async function runArtifactPublishCommand(
|
|
|
188
200
|
const loadCapabilities = deps.loadCapabilities ?? defaultLoadCapabilities;
|
|
189
201
|
const capabilities = await loadCapabilities(context);
|
|
190
202
|
|
|
203
|
+
let git: GitOps;
|
|
191
204
|
const finishContext: FinishContext = {
|
|
192
205
|
companionPath: context.companionPath,
|
|
193
206
|
repositoryId: context.repositoryId,
|
|
194
207
|
repository: context.repository,
|
|
208
|
+
// Resolution runs only after `git` is set below.
|
|
209
|
+
changedPaths: () => git.changedPaths(),
|
|
210
|
+
holdCache: publicationHoldCache(),
|
|
195
211
|
};
|
|
196
212
|
const finisher = finisherFactory(finishContext);
|
|
197
213
|
if (!finisher.isEnabled(capabilities)) {
|
|
@@ -200,7 +216,6 @@ export async function runArtifactPublishCommand(
|
|
|
200
216
|
return;
|
|
201
217
|
}
|
|
202
218
|
|
|
203
|
-
let git: GitOps;
|
|
204
219
|
try {
|
|
205
220
|
git = (deps.git ?? defaultGitOps)(
|
|
206
221
|
context.companionPath,
|
|
@@ -220,11 +235,13 @@ export async function runArtifactPublishCommand(
|
|
|
220
235
|
* suppressed; every other mode lets the engine report each publication as it finishes.
|
|
221
236
|
*/
|
|
222
237
|
const quiet = allMode && json;
|
|
238
|
+
const scanPublicationFn = deps.scanPublication ?? scanPublication;
|
|
223
239
|
const engineDeps = {
|
|
224
240
|
git,
|
|
225
241
|
json: !allMode && json,
|
|
226
242
|
stdout: quiet ? (): void => {} : emitOut,
|
|
227
243
|
stderr: quiet ? (): void => {} : emitErr,
|
|
244
|
+
secretScan: (commitPaths: string[]) => scanPublicationFn(context.companionPath, commitPaths),
|
|
228
245
|
};
|
|
229
246
|
|
|
230
247
|
const runChange = (target: string): Promise<FinishResult> =>
|
|
@@ -253,6 +270,19 @@ export async function runArtifactPublishCommand(
|
|
|
253
270
|
const archives = await discoverArchives(context.companionPath, changed, changeKinds);
|
|
254
271
|
|
|
255
272
|
for (const entry of pendingArchives(archives)) {
|
|
273
|
+
// A held-back archive waits for its merge; it does not stop the merged ones.
|
|
274
|
+
// Same cache as the finisher's resolve step, so both see one pending set and agree.
|
|
275
|
+
const hold = await publicationHold(
|
|
276
|
+
context.companionPath,
|
|
277
|
+
entry.anchor,
|
|
278
|
+
async () => changed,
|
|
279
|
+
finishContext.holdCache,
|
|
280
|
+
);
|
|
281
|
+
if (hold) {
|
|
282
|
+
if (!quiet) emitErr(hold);
|
|
283
|
+
results.push(nothingToPublish(entry.anchor, hold));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
256
286
|
const result = await runChange(entry.anchor);
|
|
257
287
|
results.push(result);
|
|
258
288
|
if (halted(result)) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { describeSecretFindings, type SecretFinding } from "../../../../runtime/secret-scan";
|
|
1
2
|
import type { ArtifactFinisher, Produced } from "./finisher";
|
|
2
3
|
import type { GitOps } from "./git";
|
|
4
|
+
import type { PublicationSecretScan } from "./secret-scan";
|
|
3
5
|
|
|
4
6
|
/** Pipeline step a {@link FinishResult} refers to (generic across artifact kinds). */
|
|
5
7
|
export type FinishStep =
|
|
6
8
|
| "resolve"
|
|
7
9
|
| "branch-guard"
|
|
8
10
|
| "cap-sync"
|
|
11
|
+
| "secret-scan"
|
|
9
12
|
| "commit"
|
|
10
13
|
| "sync-remote"
|
|
11
14
|
| "tag"
|
|
@@ -31,6 +34,10 @@ export interface FinishResult {
|
|
|
31
34
|
conflictedPaths: string[];
|
|
32
35
|
local: { committed: boolean; tagged: boolean; pushed: boolean };
|
|
33
36
|
message: string;
|
|
37
|
+
/** Set when the secret scan stopped the publication: where and which rule, never the value. */
|
|
38
|
+
secretFindings?: SecretFinding[];
|
|
39
|
+
/** The finisher's warning about what it resolved; publication continued. */
|
|
40
|
+
warning?: string;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface EngineOptions {
|
|
@@ -43,13 +50,15 @@ export interface EngineDeps {
|
|
|
43
50
|
json: boolean;
|
|
44
51
|
stdout: (line: string) => void;
|
|
45
52
|
stderr: (line: string) => void;
|
|
53
|
+
/** Scans the staged outputs for secrets; absent means no scan step. */
|
|
54
|
+
secretScan?: (commitPaths: string[]) => Promise<PublicationSecretScan>;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
58
|
* Runs the fixed publication pipeline for a resolved {@link ArtifactFinisher}:
|
|
50
|
-
* resolve → branch guard → cap sync → scoped commit → sync-remote → tag →
|
|
51
|
-
* git, remote-sync, and conflict-handoff machinery is identical for every
|
|
52
|
-
* kind — only the finisher's resolution and cap sync vary.
|
|
59
|
+
* resolve → branch guard → cap sync → secret scan → scoped commit → sync-remote → tag →
|
|
60
|
+
* push. The git, remote-sync, and conflict-handoff machinery is identical for every
|
|
61
|
+
* artifact kind — only the finisher's resolution and cap sync vary.
|
|
53
62
|
*
|
|
54
63
|
* Isolation is a clean abort, not a rollback: the resolved artifact is durable input
|
|
55
64
|
* the pipeline never produced, so no step resets, restores, or deletes a path.
|
|
@@ -111,6 +120,10 @@ export async function runFinishEngine(
|
|
|
111
120
|
}
|
|
112
121
|
const resolved: Produced = resolution.resolved;
|
|
113
122
|
result.anchorName = resolved.anchorName;
|
|
123
|
+
if (resolved.warning) {
|
|
124
|
+
result.warning = resolved.warning;
|
|
125
|
+
if (!deps.json) deps.stderr(`warning: ${resolved.warning}`);
|
|
126
|
+
}
|
|
114
127
|
const baseTag = `${resolved.tagNamespace ?? finisher.type}/${resolved.anchorName}`;
|
|
115
128
|
// Provisional: a `suffix` publication only learns its final name at the tag
|
|
116
129
|
// step, once the remote sync has revealed tags created elsewhere. Early exits
|
|
@@ -151,6 +164,37 @@ export async function runFinishEngine(
|
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
// Secret scan — the last pre-commit step, over exactly what the commit would stage.
|
|
168
|
+
// A finding or a scanner failure stops here like any pre-commit failure: no commit,
|
|
169
|
+
// no tag, no push, and the archive stays on disk.
|
|
170
|
+
if (deps.secretScan) {
|
|
171
|
+
result.step = "secret-scan";
|
|
172
|
+
let scan: PublicationSecretScan;
|
|
173
|
+
try {
|
|
174
|
+
scan = await deps.secretScan(resolved.commitPaths);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
fail(
|
|
177
|
+
"secret-scan",
|
|
178
|
+
`mate: the secret scan failed (${String(err)}); nothing was committed — re-run \`mate artifact publish\` to retry.`,
|
|
179
|
+
);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
if (scan.skipped) {
|
|
183
|
+
deps.stderr("mate: secret scan skipped (secrets.guardrails is false in framework.yaml).");
|
|
184
|
+
} else if (scan.findings.length > 0) {
|
|
185
|
+
result.secretFindings = scan.findings;
|
|
186
|
+
fail(
|
|
187
|
+
"secret-scan",
|
|
188
|
+
[
|
|
189
|
+
"mate: refusing to publish: the staged outputs contain secrets. Nothing was committed; the archive was left in place.",
|
|
190
|
+
...describeSecretFindings(scan.findings).map((line) => ` ${line}`),
|
|
191
|
+
"Replace each value with a placeholder or variable name, then re-run `mate artifact publish`.",
|
|
192
|
+
].join("\n"),
|
|
193
|
+
);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
154
198
|
// Commit — stage only the finisher's scoped paths. Nothing to stage means a prior
|
|
155
199
|
// publication already committed this anchor, which is a resume rather than an error.
|
|
156
200
|
result.step = "commit";
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { CapabilityConfig, LinkedRepository } from "../../../../lib/orchestrator/types";
|
|
2
|
+
import type { PublicationHoldCache } from "./openspec";
|
|
2
3
|
|
|
3
4
|
export interface FinishContext {
|
|
4
5
|
companionPath: string;
|
|
5
6
|
repositoryId: string;
|
|
6
7
|
repository?: LinkedRepository;
|
|
8
|
+
/**
|
|
9
|
+
* The companion's uncommitted paths (porcelain). Without it, publication cannot tell
|
|
10
|
+
* which other archives are pending and skips the shared-spec check.
|
|
11
|
+
*/
|
|
12
|
+
changedPaths?: () => Promise<string[]>;
|
|
13
|
+
/** Shared by every publication of one run; a fresh memo per call without it. */
|
|
14
|
+
holdCache?: PublicationHoldCache;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
@@ -26,6 +34,8 @@ export interface Produced {
|
|
|
26
34
|
* run reuses under either value: its commit is the tagged one already.
|
|
27
35
|
*/
|
|
28
36
|
tagCollision?: "reuse" | "suffix";
|
|
37
|
+
/** Published anyway, but reported: e.g. a legacy archive whose merge was not checked. */
|
|
38
|
+
warning?: string;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/** Outcome of {@link ArtifactFinisher.resolve}; the failure carries the user-facing refusal. */
|