golden-hoop-spell-opencode 0.1.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/README.md +184 -0
- package/package.json +51 -0
- package/shared/SPIKE_RESULTS.md +597 -0
- package/shared/agents/ghs-context-haiku.md.template +124 -0
- package/shared/agents/ghs-plan-designer.md.template +128 -0
- package/shared/agents/ghs-plan-reviewer.md.template +170 -0
- package/shared/assets/features.json +67 -0
- package/shared/assets/progress.md +35 -0
- package/shared/ghs.default.json +7 -0
- package/shared/ghs.default.json.notes.md +34 -0
- package/shared/ghs.json.example +7 -0
- package/shared/opencode.json.example +11 -0
- package/shared/references/coding-agent.md +533 -0
- package/shared/references/context-snapshot-guide.md +98 -0
- package/shared/references/examples.md +299 -0
- package/shared/references/plan-designer.md +163 -0
- package/shared/references/plan-reviewer.md +193 -0
- package/shared/references/sprint-agent.md +261 -0
- package/src/index.ts +9 -0
- package/src/lib/assets.ts +31 -0
- package/src/lib/codegraph.ts +66 -0
- package/src/lib/config.ts +278 -0
- package/src/lib/nonce.ts +56 -0
- package/src/lib/parse.ts +175 -0
- package/src/lib/paths.ts +26 -0
- package/src/lib/project.ts +28 -0
- package/src/lib/scripts/append-progress-session.ts +178 -0
- package/src/lib/scripts/append-sprint.ts +121 -0
- package/src/lib/scripts/archive-sprint.ts +583 -0
- package/src/lib/scripts/init-project.ts +291 -0
- package/src/lib/scripts/parallel-utils.ts +380 -0
- package/src/lib/scripts/parse-completion-signal.ts +584 -0
- package/src/lib/scripts/parse-delimited-output.ts +632 -0
- package/src/lib/scripts/resolve-project-dir.ts +130 -0
- package/src/lib/scripts/status.ts +292 -0
- package/src/lib/scripts/update-feature-status.ts +169 -0
- package/src/lib/scripts/validate-structure.ts +290 -0
- package/src/lib/state.ts +305 -0
- package/src/plugin.ts +76 -0
- package/src/prompts/context-codegraph.ts +65 -0
- package/src/prompts/context-grep.ts +68 -0
- package/src/prompts/feature-impl.ts +78 -0
- package/src/prompts/plan-designer.ts +59 -0
- package/src/prompts/plan-reviewer.ts +61 -0
- package/src/prompts/sprint-planning.ts +47 -0
- package/src/tools/archive.ts +278 -0
- package/src/tools/code.ts +448 -0
- package/src/tools/config.ts +182 -0
- package/src/tools/force-archive.ts +195 -0
- package/src/tools/init.ts +193 -0
- package/src/tools/plan-finalize.ts +333 -0
- package/src/tools/plan-review.ts +759 -0
- package/src/tools/plan-start.ts +232 -0
- package/src/tools/sprint.ts +213 -0
- package/src/tools/status.ts +51 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// `ghs-force-archive` tool — destructive archive of ALL sprints.
|
|
2
|
+
//
|
|
3
|
+
// Unlike `ghs-archive`, which only touches `status: completed` sprints, this
|
|
4
|
+
// tool moves every sprint (including in_progress / planning / blocked ones)
|
|
5
|
+
// into `.ghs/archived/`. It is the "I know what I'm doing, wipe it all"
|
|
6
|
+
// escape hatch.
|
|
7
|
+
//
|
|
8
|
+
// Because that's destructive, we gate it behind a transcription nonce:
|
|
9
|
+
// - The user first calls `ghs-archive` (any mode). When there are any
|
|
10
|
+
// incomplete sprints, `ghs-archive` issues a random alphanumeric nonce
|
|
11
|
+
// and writes it to `.ghs/.force-archive-nonce`, surfacing it in the tool
|
|
12
|
+
// result.
|
|
13
|
+
// - The user then calls this tool with the nonce transcribed into the
|
|
14
|
+
// `transcription` arg. We read the nonce file, verify via
|
|
15
|
+
// `verifyTranscribeNonce`, and only then proceed.
|
|
16
|
+
// - The nonce is consumed (file deleted) on a successful verification so a
|
|
17
|
+
// captured nonce can't be replayed.
|
|
18
|
+
//
|
|
19
|
+
// Per the feature's `technical_notes` this is weaker than the source plugin's
|
|
20
|
+
// `AskUserQuestion` sync-block (a nonce is guessable by a determined LLM),
|
|
21
|
+
// but it satisfies the AC: "missing or incorrect transcription → error +
|
|
22
|
+
// no archive; matching transcription → archive".
|
|
23
|
+
|
|
24
|
+
import { tool } from "@opencode-ai/plugin";
|
|
25
|
+
import type { ToolContext } from "@opencode-ai/plugin/tool";
|
|
26
|
+
import { resolve, join } from "node:path";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { readFile } from "node:fs/promises";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
archiveSprints,
|
|
32
|
+
getAllSprints,
|
|
33
|
+
formatArchiveReport,
|
|
34
|
+
type ArchivedSprintInfo,
|
|
35
|
+
} from "../lib/scripts/archive-sprint.ts";
|
|
36
|
+
import { verifyTranscribeNonce } from "../lib/nonce.ts";
|
|
37
|
+
import { resolveProjectDir } from "../lib/project.ts";
|
|
38
|
+
import { readNonce, nonceFilePath } from "./archive.ts";
|
|
39
|
+
|
|
40
|
+
/** Load features.json. Returns null if missing. */
|
|
41
|
+
async function loadFeaturesData(
|
|
42
|
+
projectDir: string,
|
|
43
|
+
): Promise<Record<string, unknown> | null> {
|
|
44
|
+
const path = join(resolve(projectDir), ".ghs", "features.json");
|
|
45
|
+
if (!existsSync(path)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const text = await readFile(path, "utf8");
|
|
49
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `ghs-force-archive` tool definition. Registered under the
|
|
54
|
+
* `ghs-force-archive` key.
|
|
55
|
+
*/
|
|
56
|
+
export const forceArchiveTool = tool({
|
|
57
|
+
description:
|
|
58
|
+
"⚠️ Destructive: archive ALL sprints regardless of status (including in_progress / planning / blocked). " +
|
|
59
|
+
"Use `ghs-archive` instead for the normal completed-sprint flow. " +
|
|
60
|
+
"Requires a `transcription` token — call `ghs-archive` first; when incomplete sprints remain it will " +
|
|
61
|
+
"issue a nonce, which you transcribe back here to confirm. Without a matching transcription the tool " +
|
|
62
|
+
"refuses to archive anything.",
|
|
63
|
+
args: {
|
|
64
|
+
project_dir: tool.schema
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe(
|
|
68
|
+
"Absolute path of the project root. Defaults to the opencode session's worktree/directory.",
|
|
69
|
+
),
|
|
70
|
+
transcription: tool.schema
|
|
71
|
+
.string()
|
|
72
|
+
.describe(
|
|
73
|
+
"The nonce token issued by a prior `ghs-archive` call (when incomplete sprints existed). " +
|
|
74
|
+
"Must match the issued nonce (case-insensitive, whitespace-trimmed) for the archive to proceed.",
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
async execute(
|
|
78
|
+
args: { project_dir?: string; transcription: string },
|
|
79
|
+
ctx: ToolContext,
|
|
80
|
+
): Promise<string> {
|
|
81
|
+
const projectDir = args.project_dir
|
|
82
|
+
? resolve(args.project_dir)
|
|
83
|
+
: resolveProjectDir(ctx);
|
|
84
|
+
|
|
85
|
+
// ----- Pre-flight: does the project even have features.json? -----
|
|
86
|
+
const features = await loadFeaturesData(projectDir);
|
|
87
|
+
if (!features) {
|
|
88
|
+
return [
|
|
89
|
+
"=== Sprint Archiver (force) ===",
|
|
90
|
+
"",
|
|
91
|
+
`Project directory: ${projectDir}`,
|
|
92
|
+
"",
|
|
93
|
+
"❌ features.json not found. Run `ghs-init` first.",
|
|
94
|
+
].join("\n") + "\n";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const allSprints = getAllSprints(features);
|
|
98
|
+
if (allSprints.length === 0) {
|
|
99
|
+
return formatArchiveReport({
|
|
100
|
+
projectDir,
|
|
101
|
+
mode: "archive",
|
|
102
|
+
force: true,
|
|
103
|
+
sprintsConsidered: [],
|
|
104
|
+
archived: [],
|
|
105
|
+
remainingCount: 0,
|
|
106
|
+
resetProgress: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ----- Nonce gate -----
|
|
111
|
+
const issuedNonce = await readNonce(projectDir, /* consume: */ false);
|
|
112
|
+
if (issuedNonce === null) {
|
|
113
|
+
return [
|
|
114
|
+
"❌ ghs-force-archive: no transcription nonce on file.",
|
|
115
|
+
"",
|
|
116
|
+
"Call `ghs-archive` first; when there are incomplete sprints it will issue a nonce token.",
|
|
117
|
+
"Then transcribe that token back as the `transcription` arg of this tool.",
|
|
118
|
+
"",
|
|
119
|
+
`Expected nonce file: ${nonceFilePath(projectDir)}`,
|
|
120
|
+
].join("\n") + "\n";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ok = verifyTranscribeNonce(issuedNonce, args.transcription);
|
|
124
|
+
if (!ok) {
|
|
125
|
+
return [
|
|
126
|
+
"❌ ghs-force-archive: transcription does not match the issued nonce.",
|
|
127
|
+
"",
|
|
128
|
+
"No files were modified. To retry:",
|
|
129
|
+
" 1. Call `ghs-archive` (any mode) to get a fresh nonce, OR",
|
|
130
|
+
` 2. Re-transcribe the existing nonce (case-insensitive, trimmed) into the \`transcription\` arg.`,
|
|
131
|
+
].join("\n") + "\n";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Gate passed — consume the nonce so it can't be replayed.
|
|
135
|
+
await readNonce(projectDir, /* consume: */ true);
|
|
136
|
+
|
|
137
|
+
// ----- Force archive -----
|
|
138
|
+
// We collect the preview first (force + dry-run) so the report can show
|
|
139
|
+
// sprintsConsidered, then run the real archive.
|
|
140
|
+
const preview = await archiveSprints({
|
|
141
|
+
projectDir,
|
|
142
|
+
dryRun: true,
|
|
143
|
+
force: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const archived: ArchivedSprintInfo[] = await archiveSprints({
|
|
147
|
+
projectDir,
|
|
148
|
+
dryRun: false,
|
|
149
|
+
force: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// After force-archive there are no sprints left → resetProgress is true
|
|
153
|
+
// when archiveSprints actually moved something.
|
|
154
|
+
const featuresAfter = await loadFeaturesData(projectDir);
|
|
155
|
+
const remainingCount = featuresAfter
|
|
156
|
+
? getAllSprints(featuresAfter).length
|
|
157
|
+
: 0;
|
|
158
|
+
const resetProgress = archived.length > 0 && remainingCount === 0;
|
|
159
|
+
|
|
160
|
+
// sprintsConsidered is `Record<string, unknown>[]` which matches the
|
|
161
|
+
// `Sprint[]` param of formatArchiveReport (Sprint === JsonObject
|
|
162
|
+
// internally).
|
|
163
|
+
const sprintsConsidered = preview.map((info) => ({
|
|
164
|
+
id: info.sprint_id,
|
|
165
|
+
name: info.sprint_name,
|
|
166
|
+
status: info.sprint_status,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
if (archived.length === 0) {
|
|
170
|
+
// Shouldn't normally happen (we checked allSprints.length > 0 above)
|
|
171
|
+
// but guard anyway — return the canonical "no sprints" report.
|
|
172
|
+
return formatArchiveReport({
|
|
173
|
+
projectDir,
|
|
174
|
+
mode: "archive",
|
|
175
|
+
force: true,
|
|
176
|
+
sprintsConsidered,
|
|
177
|
+
archived: [],
|
|
178
|
+
remainingCount,
|
|
179
|
+
resetProgress,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const report = formatArchiveReport({
|
|
184
|
+
projectDir,
|
|
185
|
+
mode: "archive",
|
|
186
|
+
force: true,
|
|
187
|
+
sprintsConsidered,
|
|
188
|
+
archived,
|
|
189
|
+
remainingCount,
|
|
190
|
+
resetProgress,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return report;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// `ghs-init` tool — initialise the `.ghs/` tracking files for a host project.
|
|
2
|
+
//
|
|
3
|
+
// This is the entry point a user (or the AI on the user's behalf) calls to
|
|
4
|
+
// bootstrap ghs in a project. It:
|
|
5
|
+
// 1. Resolves the target project dir (explicit `project_dir` arg wins;
|
|
6
|
+
// otherwise `resolveProjectDir(ctx)` reads the opencode session's
|
|
7
|
+
// worktree/directory).
|
|
8
|
+
// 2. Calls `initProject` to create `.ghs/features.json` + `.ghs/progress.md`
|
|
9
|
+
// from the shared templates + update `.gitignore`.
|
|
10
|
+
// 3. Calls `validateFeaturesJson` on the freshly-created features.json so
|
|
11
|
+
// the AI sees a confirmation (or, in the unlikely case the shared
|
|
12
|
+
// template is corrupt, an error) inline.
|
|
13
|
+
// 4. Copies `shared/ghs.default.json` to `<projectDir>/.ghs/ghs.json` when
|
|
14
|
+
// the user doesn't already have one — so they have a starting point to
|
|
15
|
+
// customise model IDs (R3).
|
|
16
|
+
// 5. Calls `syncAgents` to render the 3 agent markdown files into
|
|
17
|
+
// `<projectDir>/.opencode/agents/`. This is a direct function import,
|
|
18
|
+
// NOT a tool invocation (per plan §3.4 D2).
|
|
19
|
+
//
|
|
20
|
+
// All file I/O is pure — no LLM calls. The returned string is what the AI
|
|
21
|
+
// sees as the tool result.
|
|
22
|
+
|
|
23
|
+
import { tool } from "@opencode-ai/plugin";
|
|
24
|
+
import type { ToolContext } from "@opencode-ai/plugin/tool";
|
|
25
|
+
import { join, resolve } from "node:path";
|
|
26
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
initProject,
|
|
30
|
+
InitFilesExistError,
|
|
31
|
+
} from "../lib/scripts/init-project.ts";
|
|
32
|
+
import {
|
|
33
|
+
validateFeaturesJson,
|
|
34
|
+
formatValidationReport,
|
|
35
|
+
} from "../lib/scripts/validate-structure.ts";
|
|
36
|
+
import { syncAgents } from "../lib/config.ts";
|
|
37
|
+
import { pluginRoot } from "../lib/paths.ts";
|
|
38
|
+
import { resolveProjectDir } from "../lib/project.ts";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copy the plugin's `shared/ghs.default.json` to `<projectDir>/.ghs/ghs.json`
|
|
42
|
+
* when the user doesn't already have one. Returns true when a copy happened,
|
|
43
|
+
* false when the destination already existed.
|
|
44
|
+
*/
|
|
45
|
+
async function seedGhsJsonIfMissing(
|
|
46
|
+
projectDir: string,
|
|
47
|
+
pluginRootDir: string,
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
const dest = join(resolve(projectDir), ".ghs", "ghs.json");
|
|
50
|
+
const destFile = Bun.file(dest);
|
|
51
|
+
if (await destFile.exists()) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const src = join(pluginRootDir, "shared", "ghs.default.json");
|
|
55
|
+
// Ensure `.ghs/` exists (initProject already created it, but be defensive
|
|
56
|
+
// — this function is also callable on a project where only .opencode/
|
|
57
|
+
// exists).
|
|
58
|
+
await mkdir(join(resolve(projectDir), ".ghs"), { recursive: true });
|
|
59
|
+
await copyFile(src, dest);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `ghs-init` tool definition. Registered by the plugin entry point under
|
|
65
|
+
* the `ghs-init` key (hyphenated, per spike 001 / D1).
|
|
66
|
+
*/
|
|
67
|
+
export const initTool = tool({
|
|
68
|
+
description:
|
|
69
|
+
"Initialise the Golden Hoop Spell (ghs) tracking files for the current project. " +
|
|
70
|
+
"Creates `.ghs/features.json`, `.ghs/progress.md`, `.ghs/ghs.json` (with default model IDs), " +
|
|
71
|
+
"and the 3 subagent markdown files under `.opencode/agents/ghs-*.md`. " +
|
|
72
|
+
"Also appends `.ghs` to `.gitignore`. " +
|
|
73
|
+
"Re-run with `force: true` to overwrite existing `.ghs/features.json` and `.ghs/progress.md`.",
|
|
74
|
+
args: {
|
|
75
|
+
project_name: tool.schema
|
|
76
|
+
.string()
|
|
77
|
+
.describe("Human-readable project name (written into features.json#project.name)."),
|
|
78
|
+
description: tool.schema
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe(
|
|
82
|
+
"Optional project description. Defaults to '<project_name> project' when omitted.",
|
|
83
|
+
),
|
|
84
|
+
project_dir: tool.schema
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
"Absolute path of the project root to initialise. Defaults to the opencode session's worktree/directory.",
|
|
89
|
+
),
|
|
90
|
+
force: tool.schema
|
|
91
|
+
.boolean()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe(
|
|
94
|
+
"When true, overwrite existing `.ghs/features.json` and `.ghs/progress.md`. Default false.",
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
async execute(
|
|
98
|
+
args: {
|
|
99
|
+
project_name: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
project_dir?: string;
|
|
102
|
+
force?: boolean;
|
|
103
|
+
},
|
|
104
|
+
ctx: ToolContext,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const projectDir = args.project_dir
|
|
107
|
+
? resolve(args.project_dir)
|
|
108
|
+
: resolveProjectDir(ctx);
|
|
109
|
+
const root = pluginRoot();
|
|
110
|
+
|
|
111
|
+
// Step 1: create .ghs/features.json + .ghs/progress.md + .gitignore.
|
|
112
|
+
let initResult;
|
|
113
|
+
try {
|
|
114
|
+
initResult = await initProject({
|
|
115
|
+
projectName: args.project_name,
|
|
116
|
+
description: args.description,
|
|
117
|
+
projectDir,
|
|
118
|
+
force: args.force === true,
|
|
119
|
+
pluginRootPath: root,
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof InitFilesExistError) {
|
|
123
|
+
return [
|
|
124
|
+
"❌ ghs-init refused to overwrite existing files:",
|
|
125
|
+
"",
|
|
126
|
+
err.message,
|
|
127
|
+
"",
|
|
128
|
+
"Re-run with `force: true` to overwrite.",
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 2: validate the freshly-created features.json. In the happy path
|
|
135
|
+
// this always passes (we just wrote it from the shared template); running
|
|
136
|
+
// it here surfaces a useful confirmation to the AI and guards against a
|
|
137
|
+
// corrupt shared template.
|
|
138
|
+
const validation = await validateFeaturesJson(initResult.featuresFile);
|
|
139
|
+
const validationReport = formatValidationReport(validation);
|
|
140
|
+
|
|
141
|
+
// Step 3: seed .ghs/ghs.json from the plugin default if the user hasn't
|
|
142
|
+
// placed one yet. Returns whether a copy happened.
|
|
143
|
+
const seededGhsJson = await seedGhsJsonIfMissing(projectDir, root);
|
|
144
|
+
|
|
145
|
+
// Step 4: render the 3 subagent markdowns into .opencode/agents/.
|
|
146
|
+
const sync = await syncAgents(projectDir, root);
|
|
147
|
+
|
|
148
|
+
// Format the result string the AI sees. Keep it human-readable and
|
|
149
|
+
// include the validation outcome + the restart hint (syncAgents writes
|
|
150
|
+
// files but opencode only picks them up on next process start — spike 004).
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push("=== ghs-init complete ===");
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(`Project directory: ${initResult.outputDir}`);
|
|
155
|
+
lines.push(`Project name: ${initResult.projectName}`);
|
|
156
|
+
lines.push(`Description: ${initResult.projectDescription}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push("Files created:");
|
|
159
|
+
lines.push(` - ${initResult.featuresFile}`);
|
|
160
|
+
lines.push(` - ${initResult.progressFile}`);
|
|
161
|
+
if (initResult.gitignoreUpdated) {
|
|
162
|
+
lines.push(` - ${initResult.gitignoreFile} (appended \`.ghs\`)`);
|
|
163
|
+
} else {
|
|
164
|
+
lines.push(
|
|
165
|
+
` - ${initResult.gitignoreFile} (already contained \`.ghs\`)`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (seededGhsJson) {
|
|
169
|
+
lines.push(` - ${join(projectDir, ".ghs", "ghs.json")} (copied from plugin default)`);
|
|
170
|
+
}
|
|
171
|
+
for (const agentPath of sync.written) {
|
|
172
|
+
lines.push(` - ${agentPath}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("Resolved model IDs:");
|
|
176
|
+
lines.push(` context: ${sync.models.context}${sync.defaults_used ? " (default)" : ""}`);
|
|
177
|
+
lines.push(` designer: ${sync.models.designer}${sync.defaults_used ? " (default)" : ""}`);
|
|
178
|
+
lines.push(` reviewer: ${sync.models.reviewer}${sync.defaults_used ? " (default)" : ""}`);
|
|
179
|
+
if (sync.defaults_used) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push(
|
|
182
|
+
"ℹ️ Model IDs came from the plugin default. Edit `.ghs/ghs.json` to customise, then call `ghs-config`.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push("Restart your OpenCode session for the new agent definitions to take effect.");
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("--- features.json validation ---");
|
|
189
|
+
lines.push(validationReport);
|
|
190
|
+
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
},
|
|
193
|
+
});
|