@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,117 +1,46 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { FRAMEWORK_NAME } from "../framework";
|
|
4
|
-
import type { AdapterContext } from "../lib/orchestrator/adapters/base";
|
|
5
1
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../
|
|
2
|
+
buildCompanionGuidance as buildCompanionGuidanceWith,
|
|
3
|
+
buildCompanionPolicyXml as buildCompanionPolicyXmlWith,
|
|
4
|
+
type GuidanceContext,
|
|
5
|
+
} from "../runtime/companion-guidance";
|
|
10
6
|
import { getWrapperBinPath } from "../lib/package-paths";
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
): string {
|
|
23
|
-
const { useGraphify = false, useTokensave = false } = options;
|
|
24
|
-
|
|
25
|
-
if (!useGraphify && !useTokensave) {
|
|
26
|
-
return "";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (useGraphify && useTokensave) {
|
|
30
|
-
return `<codebase-exploration-rules priority="mandatory">
|
|
31
|
-
<path role="graphify-out">${GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT}</path>
|
|
32
|
-
<trigger>Codebase-understanding: architecture, tracing, integrations, impact, "how does X work?"</trigger>
|
|
33
|
-
<order>tokensave -> graphify -> grep/glob/read. MUST NOT skip steps.
|
|
34
|
-
1. tokensave_context first.
|
|
35
|
-
2. If tokensave is empty/file-only/irrelevant, run graphify query "<question>"; use graphify path/explain to deepen.
|
|
36
|
-
3. Use grep/glob/read only after tokensave and graphify were tried.</order>
|
|
37
|
-
<notes>Dirty graph files are expected. Use wiki/index.md for broad navigation; GRAPH_REPORT.md only if query/path/explain fall short.</notes>
|
|
38
|
-
<post-edit>After code changes, run ${FRAMEWORK_NAME} cap index.</post-edit>
|
|
39
|
-
</codebase-exploration-rules>`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (useGraphify) {
|
|
43
|
-
return `<codebase-exploration-rules priority="mandatory">
|
|
44
|
-
<path role="graphify-out">${GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT}</path>
|
|
45
|
-
<trigger>Codebase-understanding: architecture, tracing, integrations, impact, "how does X work?"</trigger>
|
|
46
|
-
<order>graphify -> grep/glob/read. MUST try graphify before raw source.
|
|
47
|
-
1. graphify query "<question>" first.
|
|
48
|
-
2. Use graphify path "<A>" "<B>" or graphify explain "<concept>" to deepen.
|
|
49
|
-
3. Use grep/glob/read only after graphify was tried.</order>
|
|
50
|
-
<notes>Dirty graph files are expected. Use wiki/index.md for broad navigation; GRAPH_REPORT.md only if query/path/explain fall short.</notes>
|
|
51
|
-
<post-edit>After code changes, run ${FRAMEWORK_NAME} cap index --graphify.</post-edit>
|
|
52
|
-
</codebase-exploration-rules>`;
|
|
53
|
-
}
|
|
8
|
+
/**
|
|
9
|
+
* The framework-side face of the guidance builders: identical output, with the
|
|
10
|
+
* running installation's wrapper bin path supplied when a caller does not name
|
|
11
|
+
* one. The builders themselves live in `runtime/` and take that path as data,
|
|
12
|
+
* because a hook shim and the OpenCode plugin build guidance with no launch and
|
|
13
|
+
* may not reach outside `runtime/`.
|
|
14
|
+
*
|
|
15
|
+
* A caller that is *not* the running installation — the projection reader —
|
|
16
|
+
* passes the projected path instead, and so calls the runtime builders direct.
|
|
17
|
+
*/
|
|
54
18
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
19
|
+
export {
|
|
20
|
+
GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT,
|
|
21
|
+
buildCodebaseExplorationGuidanceSection,
|
|
22
|
+
hasGraphifyCapability,
|
|
23
|
+
hasOpenspecCapability,
|
|
24
|
+
hasTokensaveCapability,
|
|
25
|
+
type GuidanceCapability,
|
|
26
|
+
type GuidanceContext,
|
|
27
|
+
} from "../runtime/companion-guidance";
|
|
63
28
|
|
|
64
29
|
/**
|
|
65
30
|
* Build just the `<companion-policy>` XML block: paths, CLI tools, and
|
|
66
|
-
* mandatory rules (including the capability-gated `openspec-
|
|
31
|
+
* mandatory rules (including the capability-gated `openspec-publish` rule).
|
|
67
32
|
* Does not include codebase-exploration guidance — see
|
|
68
33
|
* `buildCompanionGuidance` for the merged single-string form, or call
|
|
69
34
|
* `buildCodebaseExplorationGuidanceSection` directly when that guidance is
|
|
70
35
|
* delivered through its own channel (as OpenCode's guidance contract does).
|
|
71
36
|
*/
|
|
72
37
|
export function buildCompanionPolicyXml(
|
|
73
|
-
context:
|
|
38
|
+
context: GuidanceContext,
|
|
74
39
|
options: { wrapperBinPath?: string } = {},
|
|
75
40
|
): string {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"",
|
|
80
|
-
`<companion-policy framework="${FRAMEWORK_NAME}" priority="mandatory">`,
|
|
81
|
-
` <overview>You are operating inside the ${FRAMEWORK_NAME} companion repository.</overview>`,
|
|
82
|
-
" <context>",
|
|
83
|
-
" <paths>",
|
|
84
|
-
` <path role="working-repository" env="MATE_REPO_PATH">${context.repository.path}</path>`,
|
|
85
|
-
` <path role="companion-repository" env="MATE_ARTIFACT_PATH">${context.companionPath}</path>`,
|
|
86
|
-
` <path role="package-wrapper-bin" env="MATE_WRAPPER_BIN_PATH">${wrapperBinPath}</path>`,
|
|
87
|
-
" </paths>",
|
|
88
|
-
" <cli-tools>",
|
|
89
|
-
` <cli name="openspec" type="wrapper" invokeAs="${path.join(wrapperBinPath, "openspec")}" />`,
|
|
90
|
-
` <cli name="graphify" type="wrapper" invokeAs="${path.join(wrapperBinPath, "graphify")}" />`,
|
|
91
|
-
` <cli name="${FRAMEWORK_NAME}" type="global" invokeAs="${FRAMEWORK_NAME}" />`,
|
|
92
|
-
" </cli-tools>",
|
|
93
|
-
` <linked-repository id="${context.repository.id}" />`,
|
|
94
|
-
" </context>",
|
|
95
|
-
" <mandatory-rules>",
|
|
96
|
-
` <rule id="artifact-location" severity="critical">Agent artifacts MUST go to ${context.companionPath}, NEVER ${context.repository.path}. Artifacts include plans, specs, ADRs, todos, notes, handoffs, reasoning docs, and scratch files.</rule>`,
|
|
97
|
-
` <rule id="pre-write-classification" severity="critical">Before ANY write, classify the target as product-code or agent-artifact. If unsure, treat it as agent-artifact.</rule>`,
|
|
98
|
-
` <rule id="product-code-location" severity="critical">Product code (README, docs, source, tests) belongs in ${context.repository.path}. Agent-artifacts belong in ${context.companionPath}.</rule>`,
|
|
99
|
-
` <rule id="local-artifact-exception" severity="critical">Only write artifacts in ${context.repository.path} when the exact path is gitignored AND intentionally local-only; otherwise use ${context.companionPath}.</rule>`,
|
|
100
|
-
` <rule id="guardrail" severity="critical">Bad artifact writes to ${context.repository.path} are rejected. Classify correctly first.</rule>`,
|
|
101
|
-
` <rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: ${path.join(wrapperBinPath, "openspec")} status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>`,
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
if (hasOpenspecCapability(context.capabilities)) {
|
|
105
|
-
lines.push(
|
|
106
|
-
` <rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run ${FRAMEWORK_NAME} artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
lines.push(" </mandatory-rules>");
|
|
111
|
-
|
|
112
|
-
lines.push("</companion-policy>");
|
|
113
|
-
|
|
114
|
-
return lines.join("\n");
|
|
41
|
+
return buildCompanionPolicyXmlWith(context, {
|
|
42
|
+
wrapperBinPath: options.wrapperBinPath ?? getWrapperBinPath(),
|
|
43
|
+
});
|
|
115
44
|
}
|
|
116
45
|
|
|
117
46
|
/**
|
|
@@ -121,23 +50,10 @@ export function buildCompanionPolicyXml(
|
|
|
121
50
|
* fragment rather than delivering exploration guidance separately.
|
|
122
51
|
*/
|
|
123
52
|
export function buildCompanionGuidance(
|
|
124
|
-
context:
|
|
53
|
+
context: GuidanceContext,
|
|
125
54
|
options: { wrapperBinPath?: string } = {},
|
|
126
55
|
): string {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const tokensaveEnabled = hasTokensaveCapability(context.capabilities);
|
|
131
|
-
|
|
132
|
-
if (graphifyEnabled || tokensaveEnabled) {
|
|
133
|
-
lines.push(
|
|
134
|
-
"",
|
|
135
|
-
buildCodebaseExplorationGuidanceSection({
|
|
136
|
-
useGraphify: graphifyEnabled,
|
|
137
|
-
useTokensave: tokensaveEnabled,
|
|
138
|
-
}),
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return lines.join("\n");
|
|
56
|
+
return buildCompanionGuidanceWith(context, {
|
|
57
|
+
wrapperBinPath: options.wrapperBinPath ?? getWrapperBinPath(),
|
|
58
|
+
});
|
|
143
59
|
}
|
package/src/plugins.ts
CHANGED
|
@@ -11,7 +11,6 @@ export { createUvPlugin } from "./tools/setup/package-managers/uv";
|
|
|
11
11
|
export { createOpenspecPlugin } from "./tools/setup/capabilities/openspec";
|
|
12
12
|
export { createReactDoctorPlugin } from "./tools/setup/capabilities/react-doctor";
|
|
13
13
|
export { createTokensavePlugin } from "./tools/setup/capabilities/tokensave";
|
|
14
|
-
export { createHeadroomPlugin } from "./tools/setup/capabilities/headroom";
|
|
15
14
|
export { createRtkPlugin } from "./tools/setup/capabilities/rtk";
|
|
16
15
|
export { createGraphifyPlugin } from "./tools/setup/capabilities/graphify";
|
|
17
16
|
export { createContextModePlugin } from "./tools/setup/capabilities/context-mode";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for companion Git synchronization, kept in Mate's own state
|
|
3
|
+
* directory and keyed by companion path.
|
|
4
|
+
*
|
|
5
|
+
* It cannot live in the Companion Repository: `.mate/` content is authoritative
|
|
6
|
+
* from the upstream, and a tracked record would dirty the companion at every
|
|
7
|
+
* session start — turning "only ahead" into a permanent state. It cannot live
|
|
8
|
+
* in a Projection Root either: one companion serves several Working
|
|
9
|
+
* Repositories, and the record is a property of the companion.
|
|
10
|
+
*
|
|
11
|
+
* Every write is best-effort. An unwritable record degrades to "a
|
|
12
|
+
* synchronization is due", which is the safe direction.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { FRAMEWORK_NAME } from "./framework";
|
|
20
|
+
|
|
21
|
+
/** Long enough that consecutive sessions do no network work. */
|
|
22
|
+
export const COMPANION_SYNC_TTL_MS = 4 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/** Short enough that a repaired fork is noticed, long enough that a retry loop cannot spawn Git per write. */
|
|
25
|
+
export const FORK_VERDICT_TTL_MS = 60 * 1000;
|
|
26
|
+
|
|
27
|
+
export interface CompanionForkRecord {
|
|
28
|
+
ahead: number;
|
|
29
|
+
behind: number;
|
|
30
|
+
checkedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompanionGitRecord {
|
|
34
|
+
syncedAt?: string;
|
|
35
|
+
/** Set while the last unattended run left work for a human; cleared on completion. */
|
|
36
|
+
unfinishedReason?: string;
|
|
37
|
+
fork?: CompanionForkRecord;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `HOME` first, matching the guard: a projected hook runs with whatever the session set. */
|
|
41
|
+
export function companionGitStateRoot(homeDir?: string): string {
|
|
42
|
+
const home = homeDir || process.env.HOME || os.homedir();
|
|
43
|
+
return path.join(home, `.${FRAMEWORK_NAME}`, "companion-git-state");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function companionKey(companionPath: string): string {
|
|
47
|
+
const resolved = (() => {
|
|
48
|
+
try {
|
|
49
|
+
return fs.realpathSync(companionPath);
|
|
50
|
+
} catch {
|
|
51
|
+
return path.resolve(companionPath);
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
return createHash("sha256").update(resolved).digest("hex").slice(0, 16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function companionGitStatePath(companionPath: string, homeDir?: string): string {
|
|
58
|
+
return path.join(companionGitStateRoot(homeDir), `${companionKey(companionPath)}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function readCompanionGitRecord(
|
|
62
|
+
companionPath: string,
|
|
63
|
+
homeDir?: string,
|
|
64
|
+
): CompanionGitRecord {
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(
|
|
67
|
+
fs.readFileSync(companionGitStatePath(companionPath, homeDir), "utf8"),
|
|
68
|
+
);
|
|
69
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
70
|
+
return parsed as CompanionGitRecord;
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Best-effort: a failure to record never fails its caller. */
|
|
77
|
+
export function writeCompanionGitRecord(
|
|
78
|
+
companionPath: string,
|
|
79
|
+
record: CompanionGitRecord,
|
|
80
|
+
homeDir?: string,
|
|
81
|
+
): boolean {
|
|
82
|
+
const target = companionGitStatePath(companionPath, homeDir);
|
|
83
|
+
try {
|
|
84
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
85
|
+
fs.writeFileSync(target, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function recordCompanionSync(
|
|
93
|
+
companionPath: string,
|
|
94
|
+
at: Date = new Date(),
|
|
95
|
+
homeDir?: string,
|
|
96
|
+
): boolean {
|
|
97
|
+
const { unfinishedReason: _cleared, ...record } = readCompanionGitRecord(companionPath, homeDir);
|
|
98
|
+
return writeCompanionGitRecord(companionPath, { ...record, syncedAt: at.toISOString() }, homeDir);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clears `syncedAt`: a run that left work for a human is not a completion, and
|
|
103
|
+
* a concurrent winner's timestamp would otherwise suppress every later run —
|
|
104
|
+
* leaving this reason rendered for a whole freshness interval with nothing due
|
|
105
|
+
* to clear it.
|
|
106
|
+
*/
|
|
107
|
+
export function recordCompanionSyncUnfinished(
|
|
108
|
+
companionPath: string,
|
|
109
|
+
reason: string,
|
|
110
|
+
homeDir?: string,
|
|
111
|
+
): boolean {
|
|
112
|
+
const { syncedAt: _stale, ...record } = readCompanionGitRecord(companionPath, homeDir);
|
|
113
|
+
return writeCompanionGitRecord(companionPath, { ...record, unfinishedReason: reason }, homeDir);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function withinInterval(stamp: string | undefined, intervalMs: number, now: Date): boolean {
|
|
117
|
+
if (!stamp) return false;
|
|
118
|
+
const recorded = Date.parse(stamp);
|
|
119
|
+
if (!Number.isFinite(recorded)) return false;
|
|
120
|
+
const age = now.getTime() - recorded;
|
|
121
|
+
return age >= 0 && age < intervalMs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isCompanionSyncDue(
|
|
125
|
+
companionPath: string,
|
|
126
|
+
ttlMs: number = COMPANION_SYNC_TTL_MS,
|
|
127
|
+
now: Date = new Date(),
|
|
128
|
+
homeDir?: string,
|
|
129
|
+
): boolean {
|
|
130
|
+
return !withinInterval(readCompanionGitRecord(companionPath, homeDir).syncedAt, ttlMs, now);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function readCachedForkRecord(
|
|
134
|
+
companionPath: string,
|
|
135
|
+
ttlMs: number = FORK_VERDICT_TTL_MS,
|
|
136
|
+
now: Date = new Date(),
|
|
137
|
+
homeDir?: string,
|
|
138
|
+
): CompanionForkRecord | null {
|
|
139
|
+
const fork = readCompanionGitRecord(companionPath, homeDir).fork;
|
|
140
|
+
if (!fork || typeof fork.ahead !== "number" || typeof fork.behind !== "number") return null;
|
|
141
|
+
return withinInterval(fork.checkedAt, ttlMs, now) ? fork : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function recordForkVerdict(
|
|
145
|
+
companionPath: string,
|
|
146
|
+
verdict: { ahead: number; behind: number },
|
|
147
|
+
at: Date = new Date(),
|
|
148
|
+
homeDir?: string,
|
|
149
|
+
): boolean {
|
|
150
|
+
const record = readCompanionGitRecord(companionPath, homeDir);
|
|
151
|
+
return writeCompanionGitRecord(
|
|
152
|
+
companionPath,
|
|
153
|
+
{ ...record, fork: { ...verdict, checkedAt: at.toISOString() } },
|
|
154
|
+
homeDir,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git primitives shared by the launch preflight and the session-runtime
|
|
3
|
+
* surfaces. Upstream-ref resolution lives here because two callers depend on
|
|
4
|
+
* meaning the same ref: `CompanionGitSync` (async, injectable runner) and the
|
|
5
|
+
* hooks (synchronous `spawnSync`). The resolution is expressed once as a step
|
|
6
|
+
* generator that both drivers run, so the two can never disagree.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export interface GitResult {
|
|
11
|
+
status: number;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UpstreamTarget {
|
|
17
|
+
remote: string;
|
|
18
|
+
branch: string;
|
|
19
|
+
ref: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bounded but far above any diagnostic output; 1 MiB truncates large fetches. */
|
|
23
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/** Refs already local: no network, so a small bound is generous. */
|
|
26
|
+
export const GIT_QUERY_TIMEOUT_MS = 5_000;
|
|
27
|
+
|
|
28
|
+
export function outputLines(stdout: string): string[] {
|
|
29
|
+
return stdout
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Matches Git progress-meter lines such as `Updating files: 76% (11340/14790)` or `..., done.` */
|
|
36
|
+
const GIT_PROGRESS_LINE = /^\S.*?: +\d+% \(\d+\/\d+\)(?:, done\.)?$/;
|
|
37
|
+
|
|
38
|
+
export function stripGitProgress(text: string): string {
|
|
39
|
+
return text
|
|
40
|
+
.split(/\r\n|\r|\n/)
|
|
41
|
+
.filter((line) => !GIT_PROGRESS_LINE.test(line.trim()))
|
|
42
|
+
.join("\n")
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function describeGitFailure(result: GitResult): string {
|
|
47
|
+
return stripGitProgress(result.stderr) || stripGitProgress(result.stdout) || "unknown Git error";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isAuthenticationFailure(result: GitResult): boolean {
|
|
51
|
+
const output = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
52
|
+
return [
|
|
53
|
+
"terminal prompts disabled",
|
|
54
|
+
"could not read username",
|
|
55
|
+
"could not read password",
|
|
56
|
+
"authentication failed",
|
|
57
|
+
"permission denied (publickey)",
|
|
58
|
+
"could not open /dev/tty",
|
|
59
|
+
"can't open /dev/tty",
|
|
60
|
+
"cannot open /dev/tty",
|
|
61
|
+
].some((marker) => output.includes(marker));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Inherited `GIT_*` overrides would point Git at the session's own repository
|
|
66
|
+
* rather than the companion, so they are stripped from every invocation.
|
|
67
|
+
*/
|
|
68
|
+
export function gitEnvironment(
|
|
69
|
+
env: Record<string, string | undefined> = process.env,
|
|
70
|
+
): Record<string, string | undefined> {
|
|
71
|
+
const {
|
|
72
|
+
GIT_DIR: _gitDir,
|
|
73
|
+
GIT_WORK_TREE: _gitWorkTree,
|
|
74
|
+
GIT_COMMON_DIR: _gitCommonDir,
|
|
75
|
+
GIT_INDEX_FILE: _gitIndexFile,
|
|
76
|
+
GIT_TERMINAL_PROMPT: _gitTerminalPrompt,
|
|
77
|
+
...rest
|
|
78
|
+
} = env;
|
|
79
|
+
return { ...rest, GIT_TERMINAL_PROMPT: "0" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Never throws: an absent Git binary and an exceeded bound are ordinary results. */
|
|
83
|
+
export function runGitSync(
|
|
84
|
+
cwd: string,
|
|
85
|
+
args: readonly string[],
|
|
86
|
+
timeoutMs: number = GIT_QUERY_TIMEOUT_MS,
|
|
87
|
+
): GitResult {
|
|
88
|
+
const result = spawnSync("git", [...args], {
|
|
89
|
+
cwd,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
env: gitEnvironment(),
|
|
92
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
93
|
+
timeout: Math.max(1, timeoutMs),
|
|
94
|
+
});
|
|
95
|
+
if (result.error) {
|
|
96
|
+
return { status: 1, stdout: result.stdout ?? "", stderr: result.error.message };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
status: result.status ?? 1,
|
|
100
|
+
stdout: result.stdout ?? "",
|
|
101
|
+
stderr: result.stderr ?? "",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseRemoteRef(stdout: string): UpstreamTarget | undefined {
|
|
106
|
+
const ref = stdout.trim();
|
|
107
|
+
const separator = ref.indexOf("/");
|
|
108
|
+
if (separator <= 0 || separator === ref.length - 1) return undefined;
|
|
109
|
+
return { remote: ref.slice(0, separator), branch: ref.slice(separator + 1), ref };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The single definition of "which upstream does this companion mean":
|
|
114
|
+
* the branch's `@{u}`, then `origin/HEAD`, then an available `origin/main` or
|
|
115
|
+
* `origin/master`. Yields argv, receives the result — so a synchronous and an
|
|
116
|
+
* asynchronous driver share one decision procedure.
|
|
117
|
+
*/
|
|
118
|
+
export function* upstreamTargetSteps(): Generator<
|
|
119
|
+
readonly string[],
|
|
120
|
+
UpstreamTarget | null,
|
|
121
|
+
GitResult
|
|
122
|
+
> {
|
|
123
|
+
const upstream = yield ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"];
|
|
124
|
+
const configured = parseRemoteRef(upstream.stdout);
|
|
125
|
+
if (upstream.status === 0 && configured) return configured;
|
|
126
|
+
|
|
127
|
+
const remoteHead = yield ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"];
|
|
128
|
+
const fromRemoteHead = parseRemoteRef(remoteHead.stdout);
|
|
129
|
+
if (remoteHead.status === 0 && fromRemoteHead) return fromRemoteHead;
|
|
130
|
+
|
|
131
|
+
for (const branch of ["main", "master"]) {
|
|
132
|
+
const exists = yield ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`];
|
|
133
|
+
if (exists.status === 0) return { remote: "origin", branch, ref: `origin/${branch}` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resolveUpstreamTargetSync(
|
|
140
|
+
companionPath: string,
|
|
141
|
+
timeoutMs: number = GIT_QUERY_TIMEOUT_MS,
|
|
142
|
+
): UpstreamTarget | null {
|
|
143
|
+
const steps = upstreamTargetSteps();
|
|
144
|
+
let step = steps.next();
|
|
145
|
+
while (!step.done) {
|
|
146
|
+
step = steps.next(runGitSync(companionPath, step.value, timeoutMs));
|
|
147
|
+
}
|
|
148
|
+
return step.value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function resolveUpstreamTargetWith(
|
|
152
|
+
run: (args: readonly string[]) => Promise<GitResult>,
|
|
153
|
+
): Promise<UpstreamTarget | null> {
|
|
154
|
+
const steps = upstreamTargetSteps();
|
|
155
|
+
let step = steps.next();
|
|
156
|
+
while (!step.done) {
|
|
157
|
+
// oxlint-disable-next-line no-await-in-loop -- the steps are a decision chain
|
|
158
|
+
step = steps.next(await run(step.value));
|
|
159
|
+
}
|
|
160
|
+
return step.value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface CompanionForkState {
|
|
164
|
+
ahead: number;
|
|
165
|
+
behind: number;
|
|
166
|
+
/**
|
|
167
|
+
* Only ahead *and* behind. Ahead alone is the ordinary state of a session
|
|
168
|
+
* that has committed artifacts and not yet finished; behind alone a
|
|
169
|
+
* fast-forward resolves without a human.
|
|
170
|
+
*/
|
|
171
|
+
forked: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computed from refs already present locally — never fetches. `null` means the
|
|
176
|
+
* question could not be answered (no upstream ref, no working tree, no Git,
|
|
177
|
+
* exceeded bound), and every caller must fall through rather than refuse.
|
|
178
|
+
*/
|
|
179
|
+
export function companionForkState(
|
|
180
|
+
companionPath: string,
|
|
181
|
+
timeoutMs: number = GIT_QUERY_TIMEOUT_MS,
|
|
182
|
+
): CompanionForkState | null {
|
|
183
|
+
const target = resolveUpstreamTargetSync(companionPath, timeoutMs);
|
|
184
|
+
if (!target) return null;
|
|
185
|
+
return forkStateAgainst(companionPath, target.ref, timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function forkStateAgainst(
|
|
189
|
+
companionPath: string,
|
|
190
|
+
ref: string,
|
|
191
|
+
timeoutMs: number = GIT_QUERY_TIMEOUT_MS,
|
|
192
|
+
): CompanionForkState | null {
|
|
193
|
+
const counts = runGitSync(
|
|
194
|
+
companionPath,
|
|
195
|
+
["rev-list", "--left-right", "--count", `${ref}...HEAD`],
|
|
196
|
+
timeoutMs,
|
|
197
|
+
);
|
|
198
|
+
if (counts.status !== 0) return null;
|
|
199
|
+
|
|
200
|
+
const [behind, ahead] = counts.stdout.trim().split(/\s+/).map(Number);
|
|
201
|
+
if (!Number.isFinite(ahead) || !Number.isFinite(behind)) return null;
|
|
202
|
+
return { ahead, behind, forked: ahead > 0 && behind > 0 };
|
|
203
|
+
}
|