@uniqbit/mate-core 0.15.5 → 0.16.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +6 -4
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +47 -0
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { materializeWorkspace } from "../../../lib/orchestrator/workspace-materialize";
|
|
2
|
-
import { writeJsonStdout } from "../../write-json-stdout";
|
|
3
|
-
|
|
4
|
-
export const workspaceMaterializeCommandDeps = {
|
|
5
|
-
materializeWorkspace: (repositoryId: string, companionPath: string) =>
|
|
6
|
-
materializeWorkspace({ repositoryId, companionPath }),
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function readFlag(argv: string[], name: string): string | undefined {
|
|
10
|
-
const index = argv.indexOf(name);
|
|
11
|
-
return index >= 0 ? argv[index + 1] : undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @command mate workspace materialize
|
|
16
|
-
* @description Validates an explicit repository/companion pairing, writes
|
|
17
|
-
* the generated `.mate/workspace.code-workspace` file, and prints its path
|
|
18
|
-
* and folder order as one JSON document. Never spawns or focuses an editor.
|
|
19
|
-
* @flags
|
|
20
|
-
* - `--repository <id>` — required; the linked repository id to pair.
|
|
21
|
-
* - `--companion <path>` — required; the companion path that must link it.
|
|
22
|
-
* - `--json` — required; this command only supports JSON output.
|
|
23
|
-
*/
|
|
24
|
-
export async function runWorkspaceMaterializeCommand(argv: string[] = []): Promise<void> {
|
|
25
|
-
const repositoryId = readFlag(argv, "--repository");
|
|
26
|
-
const companionPath = readFlag(argv, "--companion");
|
|
27
|
-
|
|
28
|
-
if (!repositoryId || !companionPath || !argv.includes("--json")) {
|
|
29
|
-
process.stderr.write(
|
|
30
|
-
"mate: `workspace materialize` requires --repository <id>, --companion <path>, and --json.\n",
|
|
31
|
-
);
|
|
32
|
-
process.exitCode = 1;
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const result = await workspaceMaterializeCommandDeps.materializeWorkspace(
|
|
38
|
-
repositoryId,
|
|
39
|
-
companionPath,
|
|
40
|
-
);
|
|
41
|
-
await writeJsonStdout(result);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
44
|
-
process.exitCode = 1;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { usage } from "../../usage";
|
|
2
|
-
import { runWorkspaceListCommand } from "./list";
|
|
3
|
-
import { runWorkspaceMaterializeCommand } from "./materialize";
|
|
4
|
-
|
|
5
|
-
/** Dispatches `mate workspace <subcommand>`. Distinct from `mate companion open`. */
|
|
6
|
-
export async function runWorkspaceCommand(
|
|
7
|
-
subcommand: string | undefined,
|
|
8
|
-
argv: string[],
|
|
9
|
-
): Promise<void> {
|
|
10
|
-
switch (subcommand) {
|
|
11
|
-
case "list":
|
|
12
|
-
await runWorkspaceListCommand(argv);
|
|
13
|
-
return;
|
|
14
|
-
case "materialize":
|
|
15
|
-
await runWorkspaceMaterializeCommand(argv);
|
|
16
|
-
return;
|
|
17
|
-
default:
|
|
18
|
-
console.error(`Unknown workspace command: ${subcommand ?? ""}`);
|
|
19
|
-
console.error(usage());
|
|
20
|
-
process.exitCode = 1;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
// Nudge the agent into the mate-artifact-finish workflow after a Bash command
|
|
2
|
-
// visibly archived an OpenSpec change. Stateless PostToolUse hook: never denies
|
|
3
|
-
// or blocks; emits additionalContext only on apparent success.
|
|
4
|
-
import type { HookEnv } from "./validate-artifact-path";
|
|
5
|
-
|
|
6
|
-
export interface NudgeOutcome {
|
|
7
|
-
exitCode: number;
|
|
8
|
-
stdout: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const ARCHIVE_PATH_PATTERN =
|
|
12
|
-
/(?:^|[\s/"'`,(])openspec\/changes\/archive\/(\d{4}-\d{2}-\d{2}-[^/\s"'`,)]+)/;
|
|
13
|
-
|
|
14
|
-
// The nudge only applies when the openspec capability is enabled and OpenSpec
|
|
15
|
-
// git auto mode is on; the plugin registers the hook unconditionally, so the
|
|
16
|
-
// gate is evaluated here from launch-injected environment before any parsing.
|
|
17
|
-
export function isGateEnabled(env: HookEnv): boolean {
|
|
18
|
-
return env.MATE_OPENSPEC_ENABLED === "1" && env.MATE_GIT_AUTO_MODE === "1";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function shellSplit(command: string): string[] {
|
|
22
|
-
const parts: string[] = [];
|
|
23
|
-
let current = "";
|
|
24
|
-
let quote: string | null = null;
|
|
25
|
-
let escaping = false;
|
|
26
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
27
|
-
const char = command[index];
|
|
28
|
-
if (escaping) {
|
|
29
|
-
current += char;
|
|
30
|
-
escaping = false;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (char === "\\" && quote !== "'") {
|
|
34
|
-
const next = command[index + 1] || "";
|
|
35
|
-
if (next === "\\" || /\s/.test(next) || next === "'" || next === '"') {
|
|
36
|
-
escaping = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if ((char === "'" || char === '"') && !quote) {
|
|
41
|
-
quote = char;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (quote === char) {
|
|
45
|
-
quote = null;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const next = command[index + 1] || "";
|
|
49
|
-
const previous = command[index - 1] || "";
|
|
50
|
-
const isControlOperator =
|
|
51
|
-
!quote &&
|
|
52
|
-
(char === ";" ||
|
|
53
|
-
char === "|" ||
|
|
54
|
-
(char === "&" && next !== ">" && previous !== ">" && previous !== "<"));
|
|
55
|
-
if (isControlOperator) {
|
|
56
|
-
if (current) parts.push(current);
|
|
57
|
-
current = "";
|
|
58
|
-
if ((char === "|" || char === "&") && next === char) {
|
|
59
|
-
parts.push(char + next);
|
|
60
|
-
index += 1;
|
|
61
|
-
} else {
|
|
62
|
-
parts.push(char);
|
|
63
|
-
}
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (!quote && /\s/.test(char)) {
|
|
67
|
-
if (current) parts.push(current);
|
|
68
|
-
current = "";
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
current += char;
|
|
72
|
-
}
|
|
73
|
-
if (current) parts.push(current);
|
|
74
|
-
return parts;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function extractChangeFromPath(value: string): string | null {
|
|
78
|
-
const match = value.replaceAll("\\", "/").match(ARCHIVE_PATH_PATTERN);
|
|
79
|
-
return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Best-effort shell variable substitution, NOT a shell interpreter: a change
|
|
83
|
-
// name is often built up through variables (DEST="...archive/$TARGET"; mv a
|
|
84
|
-
// "$DEST") rather than appearing as a literal path next to mv/archive. This
|
|
85
|
-
// resolves $NAME/${NAME} references against preceding literal NAME=value
|
|
86
|
-
// assignment tokens in the same command string; unresolved references are
|
|
87
|
-
// left as-is, which keeps prior (silent) behavior for anything it can't follow.
|
|
88
|
-
const ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
89
|
-
const VARIABLE_REFERENCE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
90
|
-
|
|
91
|
-
function substituteVariables(value: string, vars: Map<string, string>): string {
|
|
92
|
-
return value.replace(VARIABLE_REFERENCE_PATTERN, (match, braced: string, bare: string) => {
|
|
93
|
-
const name = braced ?? bare;
|
|
94
|
-
return vars.has(name) ? vars.get(name)! : match;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Assignments are collected in left-to-right token order so a later
|
|
99
|
-
// assignment (e.g. DEST) can reference an earlier one (e.g. TARGET) the same
|
|
100
|
-
// way the shell would resolve it at that point in the script.
|
|
101
|
-
function collectVariableAssignments(parts: string[]): Map<string, string> {
|
|
102
|
-
const vars = new Map<string, string>();
|
|
103
|
-
for (const part of parts) {
|
|
104
|
-
const match = part.match(ASSIGNMENT_PATTERN);
|
|
105
|
-
if (!match) continue;
|
|
106
|
-
const [, name, rawValue] = match;
|
|
107
|
-
vars.set(name, substituteVariables(rawValue, vars));
|
|
108
|
-
}
|
|
109
|
-
return vars;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Shell syntax tokens are never change-name positionals: separators end the
|
|
113
|
-
// archive invocation, redirections are skipped (a bare operator also consumes
|
|
114
|
-
// its target token).
|
|
115
|
-
const SEPARATOR_PATTERN = /^(\|\|?|&&?|;)$/;
|
|
116
|
-
const REDIRECTION_PATTERN = /^(\d*(>>?|<)|&>>?)/;
|
|
117
|
-
const BARE_REDIRECTION_PATTERN = /^(\d*(>>?|<)|&>>?)$/;
|
|
118
|
-
|
|
119
|
-
export function extractArchiveCommand(command: string): string | null {
|
|
120
|
-
const parts = shellSplit(command);
|
|
121
|
-
const index = parts.findIndex(
|
|
122
|
-
(part, position) =>
|
|
123
|
-
part === "archive" &&
|
|
124
|
-
position > 0 &&
|
|
125
|
-
(parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")),
|
|
126
|
-
);
|
|
127
|
-
if (index < 0) return null;
|
|
128
|
-
const vars = collectVariableAssignments(parts);
|
|
129
|
-
for (let position = index + 1; position < parts.length; position += 1) {
|
|
130
|
-
const part = parts[position];
|
|
131
|
-
if (!part || part === "--") continue;
|
|
132
|
-
if (SEPARATOR_PATTERN.test(part)) return null;
|
|
133
|
-
if (REDIRECTION_PATTERN.test(part)) {
|
|
134
|
-
if (BARE_REDIRECTION_PATTERN.test(part)) position += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (part.startsWith("-")) {
|
|
138
|
-
if (part === "--store") position += 1;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
return substituteVariables(part, vars);
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const MOVE_COMMANDS = new Set([
|
|
147
|
-
"mv",
|
|
148
|
-
"move",
|
|
149
|
-
"move-item",
|
|
150
|
-
"cmd",
|
|
151
|
-
"cmd.exe",
|
|
152
|
-
"powershell",
|
|
153
|
-
"powershell.exe",
|
|
154
|
-
"pwsh",
|
|
155
|
-
"pwsh.exe",
|
|
156
|
-
]);
|
|
157
|
-
|
|
158
|
-
function isPython(part: string): boolean {
|
|
159
|
-
return /^python(?:\d(?:\.\d+)*)?(?:\.exe)?$/i.test(part.split(/[\\/]/).pop() || part);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function extractMoveCommand(command: string): string | null {
|
|
163
|
-
const parts = shellSplit(command);
|
|
164
|
-
const vars = collectVariableAssignments(parts);
|
|
165
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
166
|
-
if (MOVE_COMMANDS.has(parts[index].toLowerCase())) {
|
|
167
|
-
for (const part of parts.slice(index + 1)) {
|
|
168
|
-
const change = extractChangeFromPath(substituteVariables(part, vars));
|
|
169
|
-
if (change) return change;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
|
|
173
|
-
return extractChangeFromPath(substituteVariables(command, vars));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Nudge only on apparent success: stay silent on a clear failure indication,
|
|
180
|
-
// nudge when success is indicated or indeterminate.
|
|
181
|
-
export function isClearFailure(response: unknown): boolean {
|
|
182
|
-
if (!response || typeof response !== "object") return false;
|
|
183
|
-
const value = response as Record<string, unknown>;
|
|
184
|
-
if (value.success === false || value.is_error === true) return true;
|
|
185
|
-
if (typeof value.error === "string" && value.error) return true;
|
|
186
|
-
if (value.interrupted === true) return true;
|
|
187
|
-
const exitCode = value.exit_code ?? value.exitCode;
|
|
188
|
-
return typeof exitCode === "number" && exitCode !== 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function evaluate(payload: unknown, env: HookEnv): NudgeOutcome {
|
|
192
|
-
const silent: NudgeOutcome = { exitCode: 0, stdout: "" };
|
|
193
|
-
if (!isGateEnabled(env)) return silent;
|
|
194
|
-
|
|
195
|
-
const input = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
|
|
196
|
-
if (input.hook_event_name !== "PostToolUse" || input.tool_name !== "Bash") return silent;
|
|
197
|
-
|
|
198
|
-
const toolInput =
|
|
199
|
-
input.tool_input && typeof input.tool_input === "object"
|
|
200
|
-
? (input.tool_input as Record<string, unknown>)
|
|
201
|
-
: {};
|
|
202
|
-
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
203
|
-
const change = extractArchiveCommand(command) || extractMoveCommand(command);
|
|
204
|
-
if (!change) return silent;
|
|
205
|
-
if (isClearFailure(input.tool_response)) return silent;
|
|
206
|
-
|
|
207
|
-
const context =
|
|
208
|
-
`OpenSpec change ${change} was just archived. Invoke the mate-artifact-finish ` +
|
|
209
|
-
`skill, then run \`mate artifact finish "${change}" --json\` to complete the ` +
|
|
210
|
-
"finish workflow (commit, tag, and push).";
|
|
211
|
-
return {
|
|
212
|
-
exitCode: 0,
|
|
213
|
-
stdout: JSON.stringify({
|
|
214
|
-
hookSpecificOutput: {
|
|
215
|
-
hookEventName: "PostToolUse",
|
|
216
|
-
additionalContext: context,
|
|
217
|
-
},
|
|
218
|
-
}),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function readStdin(): Promise<string> {
|
|
223
|
-
const chunks: Buffer[] = [];
|
|
224
|
-
for await (const chunk of process.stdin) {
|
|
225
|
-
chunks.push(Buffer.from(chunk));
|
|
226
|
-
}
|
|
227
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Plugin-shim entry. The gate check precedes stdin parsing so a disabled gate
|
|
231
|
-
// costs one env read.
|
|
232
|
-
export async function run(): Promise<number> {
|
|
233
|
-
if (!isGateEnabled(process.env)) return 0;
|
|
234
|
-
|
|
235
|
-
let payload: unknown = {};
|
|
236
|
-
try {
|
|
237
|
-
payload = JSON.parse(await readStdin());
|
|
238
|
-
} catch {
|
|
239
|
-
payload = {};
|
|
240
|
-
}
|
|
241
|
-
const outcome = evaluate(payload, process.env);
|
|
242
|
-
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
243
|
-
return outcome.exitCode;
|
|
244
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
|
|
3
|
-
import http from "node:http";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
export interface SpawnProxyOpts {
|
|
8
|
-
port: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface WaitForProxyReachableOptions {
|
|
12
|
-
attempts?: number;
|
|
13
|
-
delayMs?: number;
|
|
14
|
-
probe?: (port: number) => Promise<boolean>;
|
|
15
|
-
sleep?: (ms: number) => Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function resolveHeadroomPort(): number {
|
|
19
|
-
const portStr = process.env.HEADROOM_PORT;
|
|
20
|
-
if (portStr) {
|
|
21
|
-
const parsed = parseInt(portStr, 10);
|
|
22
|
-
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
23
|
-
}
|
|
24
|
-
return 8787;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isProxyReachable(port: number): Promise<boolean> {
|
|
28
|
-
const readyFile = process.env.MATE_E2E_HEADROOM_PROXY_READY_FILE;
|
|
29
|
-
if (readyFile && existsSync(readyFile)) {
|
|
30
|
-
return Promise.resolve(true);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
const req = http.get({ hostname: "127.0.0.1", port, path: "/livez", timeout: 2000 }, (res) => {
|
|
35
|
-
const statusCode = res.statusCode ?? 0;
|
|
36
|
-
resolve(statusCode >= 200 && statusCode < 300);
|
|
37
|
-
res.resume();
|
|
38
|
-
});
|
|
39
|
-
req.on("error", () => resolve(false));
|
|
40
|
-
req.on("timeout", () => {
|
|
41
|
-
req.destroy();
|
|
42
|
-
resolve(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function waitForProxyReachable(
|
|
48
|
-
port: number,
|
|
49
|
-
options: WaitForProxyReachableOptions = {},
|
|
50
|
-
): Promise<boolean> {
|
|
51
|
-
const attempts = options.attempts ?? 20;
|
|
52
|
-
const delayMs = options.delayMs ?? 250;
|
|
53
|
-
const probe = options.probe ?? isProxyReachable;
|
|
54
|
-
const sleep =
|
|
55
|
-
options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
56
|
-
|
|
57
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
58
|
-
if (await probe(port)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (attempt < attempts - 1) {
|
|
63
|
-
await sleep(delayMs);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Global map to store proxy stderr for error reporting. Keyed by port number.
|
|
71
|
-
const proxyStderr = new Map<number, string>();
|
|
72
|
-
const proxyStderrLogPath = new Map<number, string>();
|
|
73
|
-
|
|
74
|
-
export function spawnProxyBackground(opts: SpawnProxyOpts, spawnFn: typeof spawn = spawn): void {
|
|
75
|
-
// Persistent memory is intentionally not enabled: headroom treats memory as
|
|
76
|
-
// opt-in (`--memory`), so a bare `proxy --port` runs compression-only with no
|
|
77
|
-
// memory DB writes.
|
|
78
|
-
const args = ["proxy", "--port", String(opts.port)];
|
|
79
|
-
|
|
80
|
-
let stderrBuffer = "";
|
|
81
|
-
const stderrLogPath = path.join(os.tmpdir(), `mate-headroom-proxy-${opts.port}.stderr.log`);
|
|
82
|
-
proxyStderrLogPath.set(opts.port, stderrLogPath);
|
|
83
|
-
const stderrFd = openSync(stderrLogPath, "w");
|
|
84
|
-
const child = spawnFn("headroom", args, {
|
|
85
|
-
detached: true,
|
|
86
|
-
stdio: ["ignore", "ignore", stderrFd],
|
|
87
|
-
env: { ...process.env, HEADROOM_OUTPUT_SHAPER: "0", HEADROOM_TELEMETRY: "off" },
|
|
88
|
-
});
|
|
89
|
-
closeSync(stderrFd);
|
|
90
|
-
|
|
91
|
-
// Tests may inject a mocked child.stderr; real detached launches use a log file.
|
|
92
|
-
if (child.stderr) {
|
|
93
|
-
child.stderr.on("data", (chunk) => {
|
|
94
|
-
stderrBuffer += chunk.toString();
|
|
95
|
-
});
|
|
96
|
-
child.stderr.on("end", () => {
|
|
97
|
-
proxyStderr.set(opts.port, stderrBuffer);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
child.unref();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getProxyStderr(port: number): string {
|
|
105
|
-
const buffered = proxyStderr.get(port);
|
|
106
|
-
if (buffered) return buffered;
|
|
107
|
-
|
|
108
|
-
const logPath = proxyStderrLogPath.get(port);
|
|
109
|
-
if (!logPath || !existsSync(logPath)) return "";
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return readFileSync(logPath, "utf8");
|
|
113
|
-
} catch {
|
|
114
|
-
return "";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { readCompanionRegistry } from "./companion-registry-reader";
|
|
4
|
-
import { pathIsDirectory } from "./repo-local-registry";
|
|
5
|
-
import { MateError } from "./types";
|
|
6
|
-
import { writeWorkspaceDocument } from "./editor";
|
|
7
|
-
|
|
8
|
-
/** Envelope version for `mate workspace materialize --json`. */
|
|
9
|
-
export const MATERIALIZED_WORKSPACE_SCHEMA_VERSION = 1;
|
|
10
|
-
|
|
11
|
-
export interface MaterializedWorkspaceV1 {
|
|
12
|
-
schemaVersion: 1;
|
|
13
|
-
workspacePath: string;
|
|
14
|
-
folders: [string, string];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class WorkspacePairingNotFoundError extends MateError {
|
|
18
|
-
constructor(repositoryId: string, companionPath: string) {
|
|
19
|
-
super(`mate: repository "${repositoryId}" is not linked by companion: ${companionPath}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class WorkspaceRootUnavailableError extends MateError {
|
|
24
|
-
constructor(
|
|
25
|
-
readonly root: "companion" | "repository",
|
|
26
|
-
readonly rootPath: string,
|
|
27
|
-
) {
|
|
28
|
-
super(`mate: ${root} path does not exist: ${rootPath}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface MaterializeWorkspaceRequest {
|
|
33
|
-
repositoryId: string;
|
|
34
|
-
companionPath: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface MaterializeWorkspaceDeps {
|
|
38
|
-
isDirectory: (candidatePath: string) => Promise<boolean>;
|
|
39
|
-
readCompanionRegistry: typeof readCompanionRegistry;
|
|
40
|
-
writeWorkspaceDocument: typeof writeWorkspaceDocument;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function defaultMaterializeWorkspaceDeps(): MaterializeWorkspaceDeps {
|
|
44
|
-
return { isDirectory: pathIsDirectory, readCompanionRegistry, writeWorkspaceDocument };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Resolves an explicit companion-path/repository-id pairing, validates both
|
|
49
|
-
* roots exist, and writes the shared workspace document — without touching
|
|
50
|
-
* the registry or spawning an editor. Distinct from `injectEditorFolder`,
|
|
51
|
-
* which additionally launches/reloads an editor CLI.
|
|
52
|
-
*/
|
|
53
|
-
export async function materializeWorkspace(
|
|
54
|
-
request: MaterializeWorkspaceRequest,
|
|
55
|
-
deps: MaterializeWorkspaceDeps = defaultMaterializeWorkspaceDeps(),
|
|
56
|
-
): Promise<MaterializedWorkspaceV1> {
|
|
57
|
-
const companionPath = path.resolve(request.companionPath);
|
|
58
|
-
|
|
59
|
-
if (!(await deps.isDirectory(companionPath))) {
|
|
60
|
-
throw new WorkspaceRootUnavailableError("companion", companionPath);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { repos } = await deps.readCompanionRegistry(companionPath).catch(() => ({ repos: [] }));
|
|
64
|
-
const repository = repos.find((repo) => repo.id === request.repositoryId);
|
|
65
|
-
if (!repository) {
|
|
66
|
-
throw new WorkspacePairingNotFoundError(request.repositoryId, companionPath);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const repositoryPath = path.resolve(repository.path);
|
|
70
|
-
if (!(await deps.isDirectory(repositoryPath))) {
|
|
71
|
-
throw new WorkspaceRootUnavailableError("repository", repositoryPath);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { workspacePath, folders } = await deps.writeWorkspaceDocument(
|
|
75
|
-
companionPath,
|
|
76
|
-
repositoryPath,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return { schemaVersion: MATERIALIZED_WORKSPACE_SCHEMA_VERSION, workspacePath, folders };
|
|
80
|
-
}
|
package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: mate-artifact-finish
|
|
3
|
-
description: Finish a completed artifact in one step via `mate artifact finish`. Use when the user wants to finish, ship, or archive-and-push a completed artifact and anchor it with a dated revert tag.
|
|
4
|
-
allowed-tools: Bash(mate:*), Bash(git:*), Bash(openspec:*)
|
|
5
|
-
license: MIT
|
|
6
|
-
compatibility: Requires the mate CLI and the openspec capability enabled.
|
|
7
|
-
metadata:
|
|
8
|
-
author: mate
|
|
9
|
-
version: "1.2"
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
Finish a completed artifact as one deterministic mate workflow and leave a dated tag that makes rollback trivial.
|
|
13
|
-
|
|
14
|
-
## Workflow
|
|
15
|
-
|
|
16
|
-
`mate artifact finish` is the deterministic, non-interactive finish pipeline.
|
|
17
|
-
|
|
18
|
-
The CLI performs normal work; only conflict recovery requires agent judgment.
|
|
19
|
-
|
|
20
|
-
## Steps
|
|
21
|
-
|
|
22
|
-
1. **Resolve the artifact name.** Use the archived change named in the triggering context. If absent, use the single active OpenSpec change from `openspec list --json`. Ask only when multiple active changes remain ambiguous.
|
|
23
|
-
|
|
24
|
-
2. **Run the CLI.**
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
mate artifact finish "<artifact-name>" --json
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Run it from the companion repository. Finish mutates only that repository; the linked working repository is capability-indexing context. Do not manually invoke `mate cap index`.
|
|
31
|
-
|
|
32
|
-
Add `--force` only if the user explicitly wants to override the not-complete guard (validation is never bypassable). Unrelated companion changes are preserved and do not require `--force`. Add `--no-push` for a local-only finish.
|
|
33
|
-
|
|
34
|
-
3. **Parse the JSON result and branch on `status`.**
|
|
35
|
-
|
|
36
|
-
For the exact field meanings and the provider-specific conflict path, read [references/openspec.md](references/openspec.md).
|
|
37
|
-
|
|
38
|
-
- **`ok`** → Report success and mention whether it resumed from an already-produced artifact.
|
|
39
|
-
- **`skipped`** → Report that the finish completed locally without pushing.
|
|
40
|
-
- **`error`** → Surface the failing step and message, then explain what local state exists.
|
|
41
|
-
- **`conflict`** → Follow the provider-specific conflict workflow in [references/openspec.md](references/openspec.md). Do not blindly rerun the finish command.
|
|
42
|
-
|
|
43
|
-
## Guardrails
|
|
44
|
-
|
|
45
|
-
- **CRITICAL — no manual finishing**: Never hand-commit or hand-tag instead of this skill. For a still-active change the finish pipeline applies delta specs itself (via `openspec archive`) — do not pre-apply them, or produce fails with "already exists". A change whose specs were already synced (e.g. via `openspec-sync-specs`) must be archived first; finish then resumes from the archive without re-applying delta specs.
|
|
46
|
-
- Always pass `--json` and parse the result; do not scrape human-readable output.
|
|
47
|
-
- Never re-run `mate artifact finish` blindly after a `conflict`.
|
|
48
|
-
- Never auto-resolve a provider-specific conflict you do not understand — ask the user.
|
|
49
|
-
- Use the JSON fields the CLI returns; do not recompute names, dates, or tags by hand.
|
|
50
|
-
- Invoke the CLI as `mate`, never through a companion-local wrapper.
|
|
51
|
-
- Only the companion repository is a finish Git target; the working repository is an index input.
|