@opengsd/gsd-core 1.16.0 → 1.18.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode generation detection (ADR-5331 D2, D5; epic #5331, Phase 1 #5337).
|
|
4
|
+
*
|
|
5
|
+
* OpenCode 1.x and 2.x share one command name (`opencode`), one set of config
|
|
6
|
+
* locations and cannot be installed side by side, so GSD's `opencode` runtime
|
|
7
|
+
* cannot tell them apart from the descriptor alone. This module answers one
|
|
8
|
+
* question — "which generation is installed?" — and adapts the two spawn-time
|
|
9
|
+
* argument shapes that differ between generations.
|
|
10
|
+
*
|
|
11
|
+
* Evidence (ADR-5331 evidence register; reproduced against the real binaries):
|
|
12
|
+
* E2 `opencode --version` prints `1.19.0` on 1.x and `opencode v2.0.27` on 2.x.
|
|
13
|
+
* E3 2.x `opencode run` has no `--dir` and no `--variant`; both exit 1 with usage.
|
|
14
|
+
* Effort travels as a `#variant` suffix on `--model provider/model#variant`.
|
|
15
|
+
*
|
|
16
|
+
* What this module deliberately does NOT do:
|
|
17
|
+
* - It does not infer 1.x from config. 2.x reads GSD's 1.x-shaped keys in place
|
|
18
|
+
* (E7), so a 1.x-shaped config proves nothing about the binary. Only 2.x-shaped
|
|
19
|
+
* keys (or `cli.json`) are evidence, and only for generation 2.
|
|
20
|
+
* - It does not guess. An unresolvable generation is `null`, and callers keep
|
|
21
|
+
* today's behavior.
|
|
22
|
+
* - It does not apply to Kilo. Kilo is a fork on the OpenCode 1.x line with its
|
|
23
|
+
* own binary (ADR-5331 D7); callers gate on the `opencode` runtime / lane slug.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.OPENCODE_V1_FREEZE_DATE = exports.OPENCODE_GENERATION_ENV = void 0;
|
|
30
|
+
exports.parseOpencodeVersion = parseOpencodeVersion;
|
|
31
|
+
exports.generationForMajor = generationForMajor;
|
|
32
|
+
exports.probeOpencodeVersion = probeOpencodeVersion;
|
|
33
|
+
exports.detectGenerationSignals = detectGenerationSignals;
|
|
34
|
+
exports.parseGenerationOverride = parseGenerationOverride;
|
|
35
|
+
exports.resolveOpencodeGeneration = resolveOpencodeGeneration;
|
|
36
|
+
exports.resetOpencodeGenerationMemo = resetOpencodeGenerationMemo;
|
|
37
|
+
exports.detectOpencodeGeneration = detectOpencodeGeneration;
|
|
38
|
+
exports.adaptOrchestratorExecForGeneration = adaptOrchestratorExecForGeneration;
|
|
39
|
+
exports.adaptLaneInputForGeneration = adaptLaneInputForGeneration;
|
|
40
|
+
exports.adaptLanePlanForGeneration = adaptLanePlanForGeneration;
|
|
41
|
+
exports.classifyOpencodeDatabase = classifyOpencodeDatabase;
|
|
42
|
+
exports.resolveOpencodeDataDir = resolveOpencodeDataDir;
|
|
43
|
+
exports.describeDataDirNotice = describeDataDirNotice;
|
|
44
|
+
exports.describeDeprecationNotice = describeDeprecationNotice;
|
|
45
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
46
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
47
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const review_lane_invocation_cjs_1 = require("./review-lane-invocation.cjs");
|
|
49
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
50
|
+
/** Environment variable that forces a generation (`1` or `2`), bypassing the probe. */
|
|
51
|
+
exports.OPENCODE_GENERATION_ENV = 'GSD_OPENCODE_GENERATION';
|
|
52
|
+
// One whole line: optional `opencode` word, optional `v`, semver with optional
|
|
53
|
+
// pre-release/build. Both observed outputs (`1.19.0`, `opencode v2.0.27`) match;
|
|
54
|
+
// log noise, banners and multi-token lines do not.
|
|
55
|
+
const VERSION_LINE = /^(?:opencode\s+)?v?(\d+)\.(\d+)\.(\d+)((?:[-+][0-9A-Za-z.+-]+)?)$/i;
|
|
56
|
+
/**
|
|
57
|
+
* Parse the stdout of `opencode --version`.
|
|
58
|
+
*
|
|
59
|
+
* Only the first non-empty line is considered, and it must be a version and
|
|
60
|
+
* nothing else. Returns `null` for anything unparseable rather than guessing.
|
|
61
|
+
*/
|
|
62
|
+
function parseOpencodeVersion(raw) {
|
|
63
|
+
if (typeof raw !== 'string')
|
|
64
|
+
return null;
|
|
65
|
+
const first = raw.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
66
|
+
if (first === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
const m = VERSION_LINE.exec(first);
|
|
69
|
+
if (!m)
|
|
70
|
+
return null;
|
|
71
|
+
const major = Number(m[1]);
|
|
72
|
+
if (!Number.isSafeInteger(major))
|
|
73
|
+
return null;
|
|
74
|
+
return { version: `${m[1]}.${m[2]}.${m[3]}${m[4]}`, major };
|
|
75
|
+
}
|
|
76
|
+
/** Map a parsed major to a generation. Major 0 is not a shipped OpenCode line → `null`. */
|
|
77
|
+
function generationForMajor(major) {
|
|
78
|
+
if (!Number.isInteger(major) || major < 1)
|
|
79
|
+
return null;
|
|
80
|
+
return major === 1 ? 1 : 2;
|
|
81
|
+
}
|
|
82
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 5000;
|
|
83
|
+
const PROBE_OUTPUT_LIMIT = 1 << 16;
|
|
84
|
+
function text(v) {
|
|
85
|
+
if (v === null || v === undefined)
|
|
86
|
+
return '';
|
|
87
|
+
return typeof v === 'string' ? v : v.toString('utf8');
|
|
88
|
+
}
|
|
89
|
+
/** Single-line, length-bounded excerpt for diagnostics; never echoes control characters. */
|
|
90
|
+
function excerpt(s) {
|
|
91
|
+
return s.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim().slice(0, 120);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run `<binary> --version` and parse the result.
|
|
95
|
+
*
|
|
96
|
+
* Argv only, never a shell: the binary name is the sole variable and is not
|
|
97
|
+
* interpolated into a command string. Never throws.
|
|
98
|
+
*/
|
|
99
|
+
function probeOpencodeVersion(opts = {}) {
|
|
100
|
+
const binary = typeof opts.binary === 'string' && opts.binary.length > 0 ? opts.binary : 'opencode';
|
|
101
|
+
const timeout = typeof opts.timeoutMs === 'number' && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
|
|
102
|
+
? opts.timeoutMs
|
|
103
|
+
: DEFAULT_PROBE_TIMEOUT_MS;
|
|
104
|
+
const spawn = opts.spawn ?? node_child_process_1.spawnSync;
|
|
105
|
+
let result;
|
|
106
|
+
try {
|
|
107
|
+
result = spawn(binary, ['--version'], {
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
timeout,
|
|
110
|
+
shell: false,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
maxBuffer: PROBE_OUTPUT_LIMIT,
|
|
113
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
114
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
115
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return { ok: false, reason: 'error', detail: excerpt(e instanceof Error ? e.message : String(e)) };
|
|
120
|
+
}
|
|
121
|
+
if (result.error) {
|
|
122
|
+
const code = result.error.code;
|
|
123
|
+
if (code === 'ENOENT' || code === 'EACCES') {
|
|
124
|
+
return { ok: false, reason: 'missing', detail: `${binary}: ${code}` };
|
|
125
|
+
}
|
|
126
|
+
if (code === 'ETIMEDOUT') {
|
|
127
|
+
return { ok: false, reason: 'timeout', detail: `${binary} --version exceeded ${timeout}ms` };
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, reason: 'error', detail: excerpt(result.error.message) };
|
|
130
|
+
}
|
|
131
|
+
if (result.signal === 'SIGTERM' || result.signal === 'SIGKILL') {
|
|
132
|
+
return { ok: false, reason: 'timeout', detail: `${binary} --version exceeded ${timeout}ms` };
|
|
133
|
+
}
|
|
134
|
+
if (result.status !== 0) {
|
|
135
|
+
return { ok: false, reason: 'nonzero', detail: `${binary} --version exited ${String(result.status)}` };
|
|
136
|
+
}
|
|
137
|
+
const raw = text(result.stdout);
|
|
138
|
+
const parsed = parseOpencodeVersion(raw);
|
|
139
|
+
if (!parsed) {
|
|
140
|
+
return { ok: false, reason: 'unparseable', detail: `unrecognized output: ${excerpt(raw)}` };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, version: parsed.version, major: parsed.major, raw: raw.trim() };
|
|
143
|
+
}
|
|
144
|
+
function isPlainObject(v) {
|
|
145
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Look for evidence that a 2.x binary has been (or is being) used with this config.
|
|
149
|
+
*
|
|
150
|
+
* Positive evidence only: `cli.json` is created by 2.x on first start, and the
|
|
151
|
+
* following keys exist only in the native 2.x shape ([migrate-v1] renames):
|
|
152
|
+
* `plugins[]`, `agents{}`, `commands{}`, `permissions[]`, `skills[]`,
|
|
153
|
+
* `providers{}`, `snapshots`, `media{}` and `mcp.servers{}`. 1.x-shaped keys are
|
|
154
|
+
* deliberately not evidence of generation 1: 2.x reads them unchanged (E7).
|
|
155
|
+
*/
|
|
156
|
+
function detectGenerationSignals(opts) {
|
|
157
|
+
const fs = opts.fs ?? { exists: (p) => node_fs_1.default.existsSync(p) };
|
|
158
|
+
const evidence = [];
|
|
159
|
+
if (typeof opts.configDir === 'string' && opts.configDir.length > 0) {
|
|
160
|
+
try {
|
|
161
|
+
if (fs.exists(node_path_1.default.join(opts.configDir, 'cli.json')))
|
|
162
|
+
evidence.push('cli.json');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* an unreadable directory is simply no evidence */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const cfg = opts.config;
|
|
169
|
+
if (isPlainObject(cfg)) {
|
|
170
|
+
if (Array.isArray(cfg['plugins']))
|
|
171
|
+
evidence.push('plugins[]');
|
|
172
|
+
if (isPlainObject(cfg['agents']))
|
|
173
|
+
evidence.push('agents{}');
|
|
174
|
+
if (isPlainObject(cfg['commands']))
|
|
175
|
+
evidence.push('commands{}');
|
|
176
|
+
if (Array.isArray(cfg['permissions']))
|
|
177
|
+
evidence.push('permissions[]');
|
|
178
|
+
if (Array.isArray(cfg['skills']))
|
|
179
|
+
evidence.push('skills[]');
|
|
180
|
+
if (isPlainObject(cfg['providers']))
|
|
181
|
+
evidence.push('providers{}');
|
|
182
|
+
if (typeof cfg['snapshots'] === 'boolean')
|
|
183
|
+
evidence.push('snapshots');
|
|
184
|
+
if (isPlainObject(cfg['media']))
|
|
185
|
+
evidence.push('media{}');
|
|
186
|
+
const mcp = cfg['mcp'];
|
|
187
|
+
if (isPlainObject(mcp) && isPlainObject(mcp['servers']))
|
|
188
|
+
evidence.push('mcp.servers{}');
|
|
189
|
+
}
|
|
190
|
+
return { generation: evidence.length > 0 ? 2 : null, evidence };
|
|
191
|
+
}
|
|
192
|
+
/** Accepts `1`, `2`, `v1`, `v2`, `opencode2`… in any case; anything else is not an override. */
|
|
193
|
+
function parseGenerationOverride(raw) {
|
|
194
|
+
if (typeof raw === 'number')
|
|
195
|
+
return raw === 1 || raw === 2 ? raw : null;
|
|
196
|
+
if (typeof raw !== 'string')
|
|
197
|
+
return null;
|
|
198
|
+
const m = /^\s*(?:opencode\s*)?v?([12])\s*$/i.exec(raw);
|
|
199
|
+
return m ? Number(m[1]) : null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Combine the evidence in precedence order: explicit override → probe → config
|
|
203
|
+
* signals → unknown. Pure; the caller supplies already-gathered evidence.
|
|
204
|
+
*/
|
|
205
|
+
function resolveOpencodeGeneration(inputs) {
|
|
206
|
+
const notes = [];
|
|
207
|
+
if (inputs.override !== undefined && inputs.override !== null && inputs.override !== '') {
|
|
208
|
+
const forced = parseGenerationOverride(inputs.override);
|
|
209
|
+
if (forced !== null) {
|
|
210
|
+
return { generation: forced, version: null, source: 'override', detail: `forced to ${forced}.x` };
|
|
211
|
+
}
|
|
212
|
+
notes.push(`ignored invalid generation override ${JSON.stringify(typeof inputs.override === 'string' ? inputs.override : typeof inputs.override).slice(0, 40)}`);
|
|
213
|
+
}
|
|
214
|
+
const probe = inputs.probe;
|
|
215
|
+
if (probe) {
|
|
216
|
+
if (probe.ok) {
|
|
217
|
+
const gen = generationForMajor(probe.major);
|
|
218
|
+
if (gen !== null) {
|
|
219
|
+
return { generation: gen, version: probe.version, source: 'probe', detail: notes.join('; ') || null };
|
|
220
|
+
}
|
|
221
|
+
notes.push(`opencode --version reported unsupported major ${probe.major}`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
notes.push(`probe ${probe.reason}: ${probe.detail}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const signals = inputs.signals;
|
|
228
|
+
if (signals && signals.generation !== null) {
|
|
229
|
+
notes.push(`2.x config signals: ${signals.evidence.join(', ')}`);
|
|
230
|
+
return { generation: signals.generation, version: null, source: 'signals', detail: notes.join('; ') };
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
generation: null,
|
|
234
|
+
version: null,
|
|
235
|
+
source: 'unknown',
|
|
236
|
+
detail: notes.length > 0 ? notes.join('; ') : 'no evidence gathered',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ─── Memoized detection for spawn-time callers ───────────────────────────────
|
|
240
|
+
let memo = null;
|
|
241
|
+
/** Test hook: forget the memoized detection. */
|
|
242
|
+
function resetOpencodeGenerationMemo() {
|
|
243
|
+
memo = null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Detect the installed generation once per process.
|
|
247
|
+
*
|
|
248
|
+
* For callers that build a `opencode run …` command line (worktree dispatch,
|
|
249
|
+
* the reviewer lane): the override environment variable wins, then one
|
|
250
|
+
* `--version` probe, memoized so a multi-lane review spawns it once.
|
|
251
|
+
*/
|
|
252
|
+
function detectOpencodeGeneration(opts = {}) {
|
|
253
|
+
const env = opts.overrideEnv ?? process.env;
|
|
254
|
+
const override = env[exports.OPENCODE_GENERATION_ENV];
|
|
255
|
+
const key = `${opts.binary ?? 'opencode'}\u0000${String(override ?? '')}`;
|
|
256
|
+
if (memo && memo.key === key)
|
|
257
|
+
return memo.value;
|
|
258
|
+
const value = resolveOpencodeGeneration({
|
|
259
|
+
override,
|
|
260
|
+
probe: parseGenerationOverride(override) !== null ? null : probeOpencodeVersion(opts),
|
|
261
|
+
});
|
|
262
|
+
memo = { key, value };
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 2.x `opencode run` has no `--dir` (E3), so a descriptor that binds the worktree
|
|
267
|
+
* through `--dir` must bind it through the spawned process's own working directory
|
|
268
|
+
* instead — the "process-cwd" shape `resolveOrchestratorExec` already supports
|
|
269
|
+
* (`cwdFlag: null`, as for Kimi Code). Every other descriptor and generation is
|
|
270
|
+
* returned untouched.
|
|
271
|
+
*/
|
|
272
|
+
function adaptOrchestratorExecForGeneration(orchestratorExec, generation) {
|
|
273
|
+
if (generation !== 2)
|
|
274
|
+
return orchestratorExec;
|
|
275
|
+
if (!orchestratorExec || typeof orchestratorExec !== 'object')
|
|
276
|
+
return orchestratorExec;
|
|
277
|
+
if (orchestratorExec.cwdFlag !== '--dir')
|
|
278
|
+
return orchestratorExec;
|
|
279
|
+
return { ...orchestratorExec, cwdFlag: null };
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Adapt an OpenCode reviewer-lane resolution input for generation 2.
|
|
283
|
+
*
|
|
284
|
+
* 2.x rejects `--variant` (E3); a reasoning effort rides on the model reference
|
|
285
|
+
* instead (`provider/model#variant`, [D8] / [D5]). So for the `opencode` lane:
|
|
286
|
+
* - the `--variant` argv is dropped;
|
|
287
|
+
* - when an effort level and a configured model both exist, the model is
|
|
288
|
+
* returned from `configGet` with `#<level>` appended (never doubled: a model
|
|
289
|
+
* that already carries a `#variant` is the operator's explicit choice);
|
|
290
|
+
* - when an effort level exists but no model is configured, nothing can carry
|
|
291
|
+
* it, so the level is dropped and a warning says so.
|
|
292
|
+
* Any other lane, and generation 1 / unknown, is returned unchanged.
|
|
293
|
+
*/
|
|
294
|
+
function adaptLaneInputForGeneration(input, generation) {
|
|
295
|
+
const warnings = [];
|
|
296
|
+
if (generation !== 2)
|
|
297
|
+
return { input, warnings };
|
|
298
|
+
const lane = input.lane;
|
|
299
|
+
if (!lane || lane.slug !== 'opencode')
|
|
300
|
+
return { input, warnings };
|
|
301
|
+
const modelKey = typeof lane.modelConfigKey === 'string' ? lane.modelConfigKey : null;
|
|
302
|
+
const effort = typeof input.effortValue === 'string' && input.effortValue.length > 0 ? input.effortValue : null;
|
|
303
|
+
const hadEffortArgs = Array.isArray(input.effortArgs) && input.effortArgs.length > 0;
|
|
304
|
+
let configGet = input.configGet;
|
|
305
|
+
if (effort !== null && modelKey !== null) {
|
|
306
|
+
const configured = (0, review_lane_invocation_cjs_1.configString)(input.configGet(modelKey));
|
|
307
|
+
if (configured === null) {
|
|
308
|
+
warnings.push(`reasoning effort '${effort}' not applied to the OpenCode reviewer: OpenCode 2.x has no --variant ` +
|
|
309
|
+
`flag and no model is configured to carry a #variant. Set a model to apply it.`);
|
|
310
|
+
}
|
|
311
|
+
else if (!configured.includes('#')) {
|
|
312
|
+
const original = input.configGet;
|
|
313
|
+
configGet = (key) => key === modelKey ? `${configured}#${effort}` : original(key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (effort !== null) {
|
|
317
|
+
warnings.push(`reasoning effort '${effort}' not applied to the OpenCode reviewer: the lane declares no model key to carry a #variant.`);
|
|
318
|
+
}
|
|
319
|
+
if (!hadEffortArgs && configGet === input.configGet && warnings.length === 0) {
|
|
320
|
+
return { input, warnings };
|
|
321
|
+
}
|
|
322
|
+
return { input: { ...input, configGet, effortArgs: [] }, warnings };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 2.x `opencode run` treats a trailing `-` as message text, not as a "read stdin" marker: the model
|
|
326
|
+
* receives `-\n<prompt>` (verified on 2.0.27, E12). Piped stdin is read either way (E4), so for a
|
|
327
|
+
* stdin-fed opencode plan on generation 2 the marker is dropped and the run is made `--standalone`.
|
|
328
|
+
* Everything else is untouched.
|
|
329
|
+
*/
|
|
330
|
+
function adaptLanePlanForGeneration(result, generation) {
|
|
331
|
+
if (generation !== 2 || !result || result.ok !== true || !result.plan)
|
|
332
|
+
return result;
|
|
333
|
+
const plan = result.plan;
|
|
334
|
+
if (plan.slug !== 'opencode' || plan.stdin === null || plan.stdin === undefined)
|
|
335
|
+
return result;
|
|
336
|
+
const argv = plan.argv;
|
|
337
|
+
if (!Array.isArray(argv))
|
|
338
|
+
return result;
|
|
339
|
+
let next = argv;
|
|
340
|
+
if (next[next.length - 1] === '-')
|
|
341
|
+
next = next.slice(0, -1);
|
|
342
|
+
// `--standalone` ("Run with a private server instead of the background service", `opencode run
|
|
343
|
+
// --help` on 2.0.27): without it `run` attaches to a long-lived managed service on a fixed port
|
|
344
|
+
// that outlives the review, serves whichever config it booted with, and fails when another
|
|
345
|
+
// process holds the port. A review is one-shot, so it gets its own server.
|
|
346
|
+
const runIdx = next.indexOf('run');
|
|
347
|
+
if (runIdx !== -1 && !next.includes('--standalone')) {
|
|
348
|
+
next = [...next.slice(0, runIdx + 1), '--standalone', ...next.slice(runIdx + 1)];
|
|
349
|
+
}
|
|
350
|
+
if (next === argv)
|
|
351
|
+
return result;
|
|
352
|
+
return { ...result, plan: { ...plan, argv: next } };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Default reader: `node:sqlite`, read-only. Its schema lives in the write-ahead log until a checkpoint,
|
|
356
|
+
* so reading the main file's bytes would see an empty database; SQLite itself merges both. The module
|
|
357
|
+
* prints an ExperimentalWarning on some Node versions, which is swallowed for the duration of the read.
|
|
358
|
+
* Any failure (module missing, file locked or corrupt) is `null`, never a throw.
|
|
359
|
+
*/
|
|
360
|
+
function readTableNames(dbPath) {
|
|
361
|
+
const emit = process.emitWarning.bind(process);
|
|
362
|
+
try {
|
|
363
|
+
process.emitWarning = () => undefined;
|
|
364
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
365
|
+
const sqlite = require('node:sqlite');
|
|
366
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
367
|
+
try {
|
|
368
|
+
return db.prepare("select name from sqlite_master where type='table'").all().map((r) => r.name);
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
db.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
process.emitWarning = emit;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Which generation(s) built an OpenCode database. 1.x keeps sessions in a `session` table; 2.x in
|
|
383
|
+
* `session_v2`. A database 2.x created from scratch has no `session` table and 1.x cannot open it; a 1.x
|
|
384
|
+
* database that 2.x later used gains the 2.x tables beside `session` and 1.x keeps working — both
|
|
385
|
+
* reproduced on 1.18.29 and 2.0.27 (ADR-5331 E8, E18). GSD only reads the schema; it never writes to this
|
|
386
|
+
* database.
|
|
387
|
+
*/
|
|
388
|
+
function classifyOpencodeDatabase(dbPath, deps = {}) {
|
|
389
|
+
const exists = deps.exists ?? ((p) => node_fs_1.default.existsSync(p));
|
|
390
|
+
if (!exists(dbPath))
|
|
391
|
+
return 'empty';
|
|
392
|
+
const names = (deps.readTables ?? readTableNames)(dbPath);
|
|
393
|
+
if (names === null)
|
|
394
|
+
return 'unknown';
|
|
395
|
+
const v1 = names.includes('session');
|
|
396
|
+
const v2 = names.includes('session_v2');
|
|
397
|
+
if (v1 && v2)
|
|
398
|
+
return 'both';
|
|
399
|
+
if (v2)
|
|
400
|
+
return 'v2';
|
|
401
|
+
if (v1)
|
|
402
|
+
return 'v1';
|
|
403
|
+
return names.length === 0 ? 'empty' : 'unknown';
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The directory holding `opencode.db`: the `data` entry of `opencode debug paths` (the binary's own
|
|
407
|
+
* answer, identical on 1.x and 2.x), else the XDG default every OpenCode generation shares.
|
|
408
|
+
*/
|
|
409
|
+
function resolveOpencodeDataDir(opts = {}) {
|
|
410
|
+
const spawn = opts.spawn ?? node_child_process_1.spawnSync;
|
|
411
|
+
const binary = typeof opts.binary === 'string' && opts.binary.length > 0 ? opts.binary : 'opencode';
|
|
412
|
+
try {
|
|
413
|
+
const r = spawn(binary, ['debug', 'paths'], {
|
|
414
|
+
encoding: 'utf8',
|
|
415
|
+
timeout: opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
416
|
+
shell: false,
|
|
417
|
+
windowsHide: true,
|
|
418
|
+
maxBuffer: PROBE_OUTPUT_LIMIT,
|
|
419
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
420
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
421
|
+
});
|
|
422
|
+
if (!r.error && r.status === 0) {
|
|
423
|
+
const m = /^data[ \t]+(.+?)[ \t]*$/m.exec(text(r.stdout));
|
|
424
|
+
if (m && m[1] && node_path_1.default.isAbsolute(m[1]))
|
|
425
|
+
return m[1];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
/* fall through to the XDG default */
|
|
430
|
+
}
|
|
431
|
+
const env = opts.env ?? process.env;
|
|
432
|
+
const home = opts.home ?? (0, shell_command_projection_cjs_1.envGet)(env, 'HOME') ?? (0, shell_command_projection_cjs_1.envGet)(env, 'USERPROFILE') ?? '';
|
|
433
|
+
const xdgData = (0, shell_command_projection_cjs_1.envGet)(env, 'XDG_DATA_HOME');
|
|
434
|
+
const base = xdgData && node_path_1.default.isAbsolute(xdgData) ? xdgData : node_path_1.default.join(home, '.local', 'share');
|
|
435
|
+
return node_path_1.default.join(base, 'opencode');
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* What to tell the operator about the data directory, given the installed generation and the schema
|
|
439
|
+
* generation(s) found there; `null` when there is nothing to say.
|
|
440
|
+
*/
|
|
441
|
+
function describeDataDirNotice(generation, kind, dataDir) {
|
|
442
|
+
if (generation === 1 && kind === 'v2') {
|
|
443
|
+
return {
|
|
444
|
+
level: 'warn',
|
|
445
|
+
text: `OpenCode 1.x is installed but its data directory (${dataDir}) was created by OpenCode 2.x. ` +
|
|
446
|
+
"1.x cannot open it (\"Database is not empty and has no session table\"). Reinstall 2.x, or restore a " +
|
|
447
|
+
'backup of the data directory taken before 2.x first ran. GSD never modifies this database.',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (generation === 1 && kind === 'both') {
|
|
451
|
+
return {
|
|
452
|
+
level: 'info',
|
|
453
|
+
text: `OpenCode 2.x has also used the data directory (${dataDir}); it keeps its sessions in its own tables ` +
|
|
454
|
+
'beside the 1.x ones, and 1.x still opens it.',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (generation === 2 && kind === 'v1') {
|
|
458
|
+
return {
|
|
459
|
+
level: 'info',
|
|
460
|
+
text: `OpenCode 2.x will add its own tables to the data directory (${dataDir}) beside the 1.x ones; ` +
|
|
461
|
+
'OpenCode 1.x still opens it afterwards. A data directory that 2.x creates from scratch, by contrast, ' +
|
|
462
|
+
'cannot be opened by 1.x.',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* End of the OpenCode 1.x bug-fix-only window (ADR-5331 D7, epic #5331 Phase 3). A maintainer decision:
|
|
469
|
+
* 1.x receives fixes until this date, after which Phase 4 removes the 1.x paths for the `opencode` runtime.
|
|
470
|
+
* Kilo is a separate runtime on the 1.x line and is not affected.
|
|
471
|
+
*/
|
|
472
|
+
exports.OPENCODE_V1_FREEZE_DATE = '2026-12-31';
|
|
473
|
+
/**
|
|
474
|
+
* The deprecation notice shown when the installed `opencode` binary is OpenCode 1.x; `null` for 2.x and for an
|
|
475
|
+
* undetected generation. The upgrade commands are the binary's own: `opencode upgrade --major` is a flag of
|
|
476
|
+
* 1.19.0 (absent from 1.18.29) ("upgrade to OpenCode 2 and remove OpenCode 1"), and the V2 installer and package replace the 1.x binary
|
|
477
|
+
* (https://opencode.ai/v2/docs/migrate-v1/).
|
|
478
|
+
*/
|
|
479
|
+
function describeDeprecationNotice(generation) {
|
|
480
|
+
if (generation !== 1)
|
|
481
|
+
return null;
|
|
482
|
+
return (`OpenCode 1.x is deprecated for GSD: it receives bug fixes only until ${exports.OPENCODE_V1_FREEZE_DATE}, after which ` +
|
|
483
|
+
'GSD supports OpenCode 2.x only. Upgrade with `opencode upgrade --major` (OpenCode 1.19 and newer), or install ' +
|
|
484
|
+
'OpenCode 2 (`curl -fsSL https://opencode.ai/v2/install | bash`, or `npm install -g @opencode/cli`) and re-run the GSD installer. ' +
|
|
485
|
+
'Your configuration is read in place by 2.x. Kilo is not affected.');
|
|
486
|
+
}
|