@opengsd/gsd-core 1.5.0-rc.1 → 1.5.0-rc.2
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/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +16 -15
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +12 -11
- package/agents/gsd-planner.md +21 -181
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +9 -8
- package/bin/install.js +205 -1402
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1820 -3
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/core.cjs +51 -1
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +61 -6
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/roadmap.cjs +40 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +51 -141
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +67 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +137 -101
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +103 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +2 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +26 -37
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +51 -16
- package/gsd-core/workflows/complete-milestone.md +1 -1
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +39 -27
- package/gsd-core/workflows/plan-review-convergence.md +1 -1
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +18 -14
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +1 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +1 -1
- package/package.json +6 -3
- package/scripts/gen-capability-registry.cjs +498 -13
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-test-file-count.allowlist.json +15 -2
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +54 -13
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
|
@@ -140,6 +140,26 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
140
140
|
phase.cmdPhaseComplete(cwd, args[2], raw);
|
|
141
141
|
return { ok: true, data: null };
|
|
142
142
|
},
|
|
143
|
+
'uat-passed': (_ctx) => {
|
|
144
|
+
let requireVerification = false;
|
|
145
|
+
const positional = [];
|
|
146
|
+
for (const token of args.slice(2)) {
|
|
147
|
+
if (token === '--require-verification') {
|
|
148
|
+
requireVerification = true;
|
|
149
|
+
}
|
|
150
|
+
else if (token === '--raw') {
|
|
151
|
+
// --raw is handled by the outer CLI layer; accepted here silently
|
|
152
|
+
}
|
|
153
|
+
else if (token.startsWith('--')) {
|
|
154
|
+
return makeInvalidArgs(token, `phase uat-passed does not support ${token}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
positional.push(token);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
phase.cmdPhaseUatPassed(cwd, positional[0], raw, { policy: { requireVerification } });
|
|
161
|
+
return { ok: true, data: null };
|
|
162
|
+
},
|
|
143
163
|
},
|
|
144
164
|
};
|
|
145
165
|
// ── Build manifest (available subcommands for UnknownCommand detection) ─────
|
|
@@ -32,6 +32,9 @@ const stateMod = require("./state.cjs");
|
|
|
32
32
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
33
33
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
34
34
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
36
|
+
const uatPredicate = require("./uat-predicate.cjs");
|
|
37
|
+
const { evaluateUatPassed } = uatPredicate;
|
|
35
38
|
const { escapeRegex, loadConfig, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error, readSubdirectories, phaseTokenMatches, ERROR_REASON, } = core;
|
|
36
39
|
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
37
40
|
const { extractFrontmatter } = frontmatterMod;
|
|
@@ -1292,6 +1295,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1292
1295
|
};
|
|
1293
1296
|
output(result, raw);
|
|
1294
1297
|
}
|
|
1298
|
+
function cmdPhaseUatPassed(cwd, phaseNum, raw, opts = {}) {
|
|
1299
|
+
if (!phaseNum) {
|
|
1300
|
+
error('phase number required for phase uat-passed');
|
|
1301
|
+
}
|
|
1302
|
+
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum);
|
|
1303
|
+
if (!phaseInfoRaw) {
|
|
1304
|
+
error(`Phase ${phaseNum} not found`);
|
|
1305
|
+
}
|
|
1306
|
+
const phaseInfo = phaseInfoRaw;
|
|
1307
|
+
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1308
|
+
const report = evaluateUatPassed(phaseFullDir, { policy: opts.policy });
|
|
1309
|
+
output({ phase: phaseNum, ...report }, raw);
|
|
1310
|
+
}
|
|
1295
1311
|
module.exports = {
|
|
1296
1312
|
cmdPhasesList,
|
|
1297
1313
|
cmdPhaseNextDecimal,
|
|
@@ -1303,5 +1319,6 @@ module.exports = {
|
|
|
1303
1319
|
cmdPhaseInsert,
|
|
1304
1320
|
cmdPhaseRemove,
|
|
1305
1321
|
cmdPhaseComplete,
|
|
1322
|
+
cmdPhaseUatPassed,
|
|
1306
1323
|
computeDependencyLevels,
|
|
1307
1324
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* probe-core — generic spec-phase probe resolution model (ADR-550 Decision 7).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the edge-probe (the first adapter) once the prohibition probe (#644)
|
|
6
|
+
* proved it the *second* adapter of the same model: one adapter is a hypothetical seam,
|
|
7
|
+
* two is a real one. This module owns everything generic — the resolution lifecycle,
|
|
8
|
+
* the status×verification re-cut, `validateResolution`/`validateRequirement`, the
|
|
9
|
+
* `analyzeCoverage(items, resolutions?, validators)` merge/rollup/orphan-reject engine,
|
|
10
|
+
* the `byVerification` rollup, and the `runProbeCli` I/O scaffold. Each probe is a thin
|
|
11
|
+
* adapter: it supplies the proposal logic (deterministic for edge, LLM-recall for
|
|
12
|
+
* prohibition) and its closed vocabularies via injected validators.
|
|
13
|
+
*
|
|
14
|
+
* Authored as strict TypeScript (`src/probe-core.cts`) and compiled by
|
|
15
|
+
* `tsc -p tsconfig.build.json` to the gitignored runtime artifact
|
|
16
|
+
* `gsd-core/bin/lib/probe-core.cjs`. Do NOT hand-write the `.cjs`; it is emitted.
|
|
17
|
+
*
|
|
18
|
+
* Two orthogonal axes (the re-cut):
|
|
19
|
+
* - status: resolved | dismissed | unresolved — the resolution LIFECYCLE (shared)
|
|
20
|
+
* - verification: <probe-defined> | null — HOW a resolved item is verified
|
|
21
|
+
* The edge adapter declares `verification: explicit | backstop`; the prohibition adapter
|
|
22
|
+
* (#644) will declare `test | judgment`. Splitting the axes keeps the lifecycle enum free
|
|
23
|
+
* of a verification fact and lets a sibling probe add its own tiers without a parallel enum.
|
|
24
|
+
*
|
|
25
|
+
* Typing is hybrid (ADR-550 #5): generic type params for adapter DX, but enforcement runs
|
|
26
|
+
* through injected runtime validators, because the CLI executes over JSON where TS types are
|
|
27
|
+
* erased. The contract test pins the validators, not the types.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.VALID_STATUS = void 0;
|
|
34
|
+
exports.validateRequirement = validateRequirement;
|
|
35
|
+
exports.validateResolution = validateResolution;
|
|
36
|
+
exports.analyzeCoverage = analyzeCoverage;
|
|
37
|
+
exports.runProbeCli = runProbeCli;
|
|
38
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
40
|
+
exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
|
|
41
|
+
function errMessage(e) {
|
|
42
|
+
return e instanceof Error ? e.message : String(e);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
|
|
46
|
+
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
|
|
47
|
+
* that forgets to validate inside its closure could hand back a malformed object. Rather than
|
|
48
|
+
* stringify garbage as green output, `runProbeCli` checks the report shape and fails closed.
|
|
49
|
+
*/
|
|
50
|
+
function isValidReport(report) {
|
|
51
|
+
if (report == null || typeof report !== 'object')
|
|
52
|
+
return false;
|
|
53
|
+
const r = report;
|
|
54
|
+
if (!Array.isArray(r.items))
|
|
55
|
+
return false;
|
|
56
|
+
const c = r.coverage;
|
|
57
|
+
if (c == null || typeof c !== 'object')
|
|
58
|
+
return false;
|
|
59
|
+
if (typeof c.applicable !== 'number' || typeof c.resolved !== 'number' || typeof c.unresolved !== 'number') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (c.byVerification == null || typeof c.byVerification !== 'object')
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate a requirement's generic structural fields — fail closed on malformed input rather
|
|
68
|
+
* than coercing it. Probe-specific fields (e.g. the edge adapter's `shapes`) are validated by
|
|
69
|
+
* the adapter. Typed loosely because the CLI casts arbitrary parsed JSON to `Requirement`.
|
|
70
|
+
*/
|
|
71
|
+
function validateRequirement(requirement) {
|
|
72
|
+
const r = requirement;
|
|
73
|
+
if (typeof r.id !== 'string' || !r.id.trim()) {
|
|
74
|
+
throw new Error(`requirement id must be a non-empty string (got ${JSON.stringify(r.id)})`);
|
|
75
|
+
}
|
|
76
|
+
if (r.text != null && typeof r.text !== 'string') {
|
|
77
|
+
throw new Error(`requirement ${r.id} text must be a string when present`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate a resolution against the probe's injected validators. Rejects an unknown status,
|
|
82
|
+
* a dismissal without a non-empty reason, a `resolved` item with a missing/unknown
|
|
83
|
+
* verification tier, and a `resolved` item missing any field its tier requires (per
|
|
84
|
+
* `requiredFieldsByVerification`). Returns true on success.
|
|
85
|
+
*/
|
|
86
|
+
function validateResolution(r, validators) {
|
|
87
|
+
const key = `${r.requirement_id}::${r.category}`;
|
|
88
|
+
if (!exports.VALID_STATUS.includes(r.status)) {
|
|
89
|
+
throw new Error(`invalid status "${r.status}" for ${key}`);
|
|
90
|
+
}
|
|
91
|
+
// Invariant (this module's header): `verification` is null unless `status` is `resolved`.
|
|
92
|
+
// Enforce it for EVERY status — a dismissed/unresolved resolution carrying a verification
|
|
93
|
+
// tier would otherwise merge verbatim (`analyzeCoverage` below) and silently break the
|
|
94
|
+
// model for the second adapter (#644) that inherits this seam. Fail closed across the full
|
|
95
|
+
// status×verification space, not just `resolved`.
|
|
96
|
+
if (r.status !== 'resolved' && r.verification != null) {
|
|
97
|
+
throw new Error(`verification must be null unless status is "resolved" (got "${r.verification}") for ${key}`);
|
|
98
|
+
}
|
|
99
|
+
// An `unresolved` resolution is an UNACTED item: it must carry no resolution/reason payload.
|
|
100
|
+
// A populated payload is an authoring mistake (the author meant resolved/dismissed) that
|
|
101
|
+
// would otherwise be silently dropped into the unresolved count with no error pointing at
|
|
102
|
+
// it. Reject it so the mistake surfaces.
|
|
103
|
+
if (r.status === 'unresolved') {
|
|
104
|
+
if (r.resolution != null && String(r.resolution).trim()) {
|
|
105
|
+
throw new Error(`unresolved must not carry a resolution (${key})`);
|
|
106
|
+
}
|
|
107
|
+
if (r.reason != null && String(r.reason).trim()) {
|
|
108
|
+
throw new Error(`unresolved must not carry a reason (${key})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (r.status === 'dismissed' && !(r.reason && String(r.reason).trim())) {
|
|
112
|
+
throw new Error(`dismissed requires a reason (${key})`);
|
|
113
|
+
}
|
|
114
|
+
if (r.status === 'resolved') {
|
|
115
|
+
const tier = r.verification;
|
|
116
|
+
if (tier == null) {
|
|
117
|
+
throw new Error(`resolved requires a verification tier (one of: ${validators.verification.join(', ')}) for ${key}`);
|
|
118
|
+
}
|
|
119
|
+
if (!validators.verification.includes(tier)) {
|
|
120
|
+
throw new Error(`invalid verification "${tier}" for ${key} — must be one of: ${validators.verification.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
const required = validators.requiredFieldsByVerification[tier] ?? [];
|
|
123
|
+
for (const field of required) {
|
|
124
|
+
// field is 'resolution' | 'reason'; both are `string | null | undefined` on Resolution,
|
|
125
|
+
// so the indexed access is string-typed (no unknown-to-string coercion).
|
|
126
|
+
const value = r[field];
|
|
127
|
+
if (!(value != null && String(value).trim())) {
|
|
128
|
+
throw new Error(`${tier} requires a ${field} (${key})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Merge author resolutions onto ALREADY-PROPOSED items and roll up coverage counts.
|
|
136
|
+
*
|
|
137
|
+
* Core operates on `items[]`, never a `proposeFn`: probes have different deterministic
|
|
138
|
+
* surfaces (edge = deterministic propose + LLM resolve; prohibition = LLM propose + deterministic
|
|
139
|
+
* validate/merge), so proposal stays in each adapter and core must not assume it is deterministic.
|
|
140
|
+
*
|
|
141
|
+
* `coverage.resolved` is the COUNT of CLOSED items (`resolved` + `dismissed` status) =
|
|
142
|
+
* `applicable - unresolved` — the pre-re-cut "covered + dismissed + backstop" set,
|
|
143
|
+
* count-preserved. `byVerification` breaks the `resolved`-status items down by tier (each tier
|
|
144
|
+
* initialized to 0). Throws on any invalid resolution, a duplicate, an orphan (a resolution
|
|
145
|
+
* matching no proposed item), or a proposed item whose category is outside `validators.categories`.
|
|
146
|
+
*/
|
|
147
|
+
function analyzeCoverage(items, resolutions = [], validators) {
|
|
148
|
+
if (!Array.isArray(items)) {
|
|
149
|
+
throw new Error('items must be an array');
|
|
150
|
+
}
|
|
151
|
+
const key = (r) => `${r.requirement_id}::${r.category}`;
|
|
152
|
+
const resMap = new Map();
|
|
153
|
+
for (const r of resolutions) {
|
|
154
|
+
validateResolution(r, validators);
|
|
155
|
+
if (resMap.has(key(r))) {
|
|
156
|
+
throw new Error(`duplicate resolution for ${key(r)}`);
|
|
157
|
+
}
|
|
158
|
+
resMap.set(key(r), r);
|
|
159
|
+
}
|
|
160
|
+
const validCategories = new Set(validators.categories);
|
|
161
|
+
const merged = [];
|
|
162
|
+
const itemKeys = new Set();
|
|
163
|
+
for (const item of items) {
|
|
164
|
+
if (!validCategories.has(item.category)) {
|
|
165
|
+
throw new Error(`item ${key(item)} has unknown category "${item.category}" — not one of: ${validators.categories.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
itemKeys.add(key(item));
|
|
168
|
+
const o = resMap.get(key(item));
|
|
169
|
+
if (o) {
|
|
170
|
+
merged.push({ ...item, status: o.status, verification: o.verification ?? null, resolution: o.resolution ?? null, reason: o.reason ?? null });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// No author resolution: the item is rolled up VERBATIM, so its own status/fields must be
|
|
174
|
+
// valid too. The edge adapter only proposes `unresolved` items, but the prohibition adapter
|
|
175
|
+
// (#644) proposes LLM-generated items that arrive already populated — one carrying an
|
|
176
|
+
// out-of-enum status (e.g. the dropped "covered") or `dismissed` with no reason would
|
|
177
|
+
// otherwise be counted closed without validation. An Item is structurally a superset of a
|
|
178
|
+
// Resolution, so the same fail-closed check guards both. (ADR-550 Decision 5 hardens this
|
|
179
|
+
// shared seam for the second adapter; m1.)
|
|
180
|
+
validateResolution(item, validators);
|
|
181
|
+
merged.push(item);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Reject orphan resolutions — a resolution whose (requirement_id, category) matches no
|
|
185
|
+
// proposed item (typo'd category or a non-applicable one) would otherwise be silently
|
|
186
|
+
// dropped, leaving the author believing an item is resolved while the report shows it
|
|
187
|
+
// unresolved (adversarial-review HIGH; preserved from the edge-probe's original engine).
|
|
188
|
+
for (const k of resMap.keys()) {
|
|
189
|
+
if (!itemKeys.has(k)) {
|
|
190
|
+
throw new Error(`unknown resolution for ${k} — no matching proposed item (typo'd category or non-applicable shape?)`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const unresolved = merged.filter((i) => i.status === 'unresolved').length;
|
|
194
|
+
const applicable = merged.length;
|
|
195
|
+
const resolved = applicable - unresolved; // closed set: resolved-status + dismissed
|
|
196
|
+
const byVerification = {};
|
|
197
|
+
for (const tier of validators.verification)
|
|
198
|
+
byVerification[tier] = 0;
|
|
199
|
+
for (const i of merged) {
|
|
200
|
+
if (i.status === 'resolved' && i.verification != null) {
|
|
201
|
+
byVerification[i.verification] = (byVerification[i.verification] ?? 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { items: merged, coverage: { applicable, resolved, unresolved, byVerification } };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
208
|
+
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|
|
209
|
+
* line to stderr and exits 2. A JSON-parse failure or any `analyze` throw is a handled error:
|
|
210
|
+
* stderr + exit 2, never an uncaught stack trace — so the engine's fail-closed validation
|
|
211
|
+
* surfaces at the workflow boundary rather than failing open.
|
|
212
|
+
*/
|
|
213
|
+
function runProbeCli(analyze, options) {
|
|
214
|
+
const argv = options.argv ?? process.argv;
|
|
215
|
+
const readFile = options.readFile ?? ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
216
|
+
const write = options.write ?? ((s) => { process.stdout.write(s); });
|
|
217
|
+
const writeErr = options.writeErr ?? ((s) => { process.stderr.write(s); });
|
|
218
|
+
const exit = options.exit ?? ((code) => { process.exit(code); });
|
|
219
|
+
const reqPath = argv[2];
|
|
220
|
+
const resPath = argv[3];
|
|
221
|
+
if (!reqPath) {
|
|
222
|
+
writeErr(`usage: ${options.usage}\n`);
|
|
223
|
+
exit(2);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let requirements;
|
|
227
|
+
try {
|
|
228
|
+
requirements = JSON.parse(readFile(reqPath));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
writeErr(`error: cannot parse JSON from ${reqPath}: ${errMessage(e)}\n`);
|
|
232
|
+
exit(2);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let resolutions = [];
|
|
236
|
+
if (resPath) {
|
|
237
|
+
try {
|
|
238
|
+
resolutions = JSON.parse(readFile(resPath));
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
writeErr(`error: cannot parse JSON from ${resPath}: ${errMessage(e)}\n`);
|
|
242
|
+
exit(2);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const report = analyze(requirements, resolutions);
|
|
248
|
+
if (!isValidReport(report)) {
|
|
249
|
+
throw new Error('adapter returned a structurally-invalid coverage report (expected { items[], coverage{ applicable, resolved, unresolved, byVerification } })');
|
|
250
|
+
}
|
|
251
|
+
write(`${JSON.stringify(report, null, 2)}\n`);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
writeErr(`error: ${errMessage(e)}\n`);
|
|
255
|
+
exit(2);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -135,6 +135,45 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
135
135
|
section,
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
+
// ─── getRoadmapPhaseWithFallback ──────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Two-pass phase lookup that mirrors cmdRoadmapGetPhase's resolution strategy.
|
|
141
|
+
*
|
|
142
|
+
* Pass 1: current-milestone slice (extractCurrentMilestone).
|
|
143
|
+
* Pass 2: full roadmap content (stripShippedMilestones) — covers cross-milestone
|
|
144
|
+
* and older frontend phases that are no longer in the current milestone slice.
|
|
145
|
+
*
|
|
146
|
+
* Returns the phase section string if found, null if ROADMAP.md is missing,
|
|
147
|
+
* or throws if ROADMAP.md read fails.
|
|
148
|
+
*
|
|
149
|
+
* Used by check-command-router (computeUiPlanGate) so ui-plan-gate uses the SAME
|
|
150
|
+
* phase resolution as `roadmap.get-phase` — not a milestone-only subset.
|
|
151
|
+
*/
|
|
152
|
+
function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
153
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
154
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
155
|
+
return null;
|
|
156
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
157
|
+
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
158
|
+
const fullContent = stripShippedMilestones(rawContent);
|
|
159
|
+
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
160
|
+
if (exactSource) {
|
|
161
|
+
const exactMilestone = searchPhaseInContent(milestoneContent, exactSource, phaseNum);
|
|
162
|
+
if (exactMilestone && !exactMilestone.error)
|
|
163
|
+
return exactMilestone.section ?? null;
|
|
164
|
+
const exactFull = searchPhaseInContent(fullContent, exactSource, phaseNum);
|
|
165
|
+
if (exactFull && !exactFull.error)
|
|
166
|
+
return exactFull.section ?? null;
|
|
167
|
+
}
|
|
168
|
+
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
169
|
+
const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
|
|
170
|
+
const result = (milestoneResult && !milestoneResult.error)
|
|
171
|
+
? milestoneResult
|
|
172
|
+
: searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
|
|
173
|
+
if (!result || result.error)
|
|
174
|
+
return null;
|
|
175
|
+
return result.section ?? null;
|
|
176
|
+
}
|
|
138
177
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
139
178
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
140
179
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
@@ -597,6 +636,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
597
636
|
}
|
|
598
637
|
module.exports = {
|
|
599
638
|
cmdRoadmapGetPhase,
|
|
639
|
+
getRoadmapPhaseWithFallback,
|
|
600
640
|
cmdRoadmapAnalyze,
|
|
601
641
|
cmdRoadmapUpdatePlanProgress,
|
|
602
642
|
cmdRoadmapAnnotateDependencies,
|
|
@@ -197,8 +197,12 @@ function kimiAgentsKind(destSubpath, prefix, configDir) {
|
|
|
197
197
|
* @param runtime canonical runtime ID (gates Hermes/Qwen branding in converter)
|
|
198
198
|
* @param configDir runtime config dir (for .gsd-source marker resolution)
|
|
199
199
|
* @param nested if true, nest concrete skills under their ns-* routers (#69)
|
|
200
|
+
* @param scope install scope; converted to isGlobal and passed as 5th positional
|
|
201
|
+
* arg so scope-aware converters (antigravity, copilot) can choose
|
|
202
|
+
* between global home paths and workspace-relative paths without
|
|
203
|
+
* colliding with the `runtime` string at position 3.
|
|
200
204
|
*/
|
|
201
|
-
function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false) {
|
|
205
|
+
function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false, scope = 'global') {
|
|
202
206
|
return {
|
|
203
207
|
kind: 'skills',
|
|
204
208
|
destSubpath,
|
|
@@ -207,9 +211,14 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nest
|
|
|
207
211
|
const installExports = getInstallExports();
|
|
208
212
|
const realConverter = installExports[converterName];
|
|
209
213
|
// Compute cmdNames once per stage call for performance (#3583).
|
|
210
|
-
// Extra args are ignored by converters that don't need
|
|
214
|
+
// Extra trailing args are ignored by converters that don't need them. The
|
|
215
|
+
// isGlobal flag is the 5th positional (NOT the 3rd): the 3rd positional is
|
|
216
|
+
// `runtime` for the claude/kimi/cline converters, so the scope-aware
|
|
217
|
+
// converters (antigravity, copilot) read isGlobal from position 5 to avoid
|
|
218
|
+
// colliding with `runtime` and always taking the global branch.
|
|
211
219
|
const cmdNames = installExports.readGsdCommandNames();
|
|
212
|
-
const
|
|
220
|
+
const isGlobal = scope === 'global';
|
|
221
|
+
const wrappedConverter = (content, skillName) => realConverter(content, skillName, runtime, cmdNames, isGlobal);
|
|
213
222
|
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested);
|
|
214
223
|
},
|
|
215
224
|
};
|
|
@@ -242,47 +251,41 @@ function convertedCommandsKind(destSubpath, prefix, converterName, configDir) {
|
|
|
242
251
|
},
|
|
243
252
|
};
|
|
244
253
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// (one-level scan under ~/.claude/skills — but Skill-tool errors on unknown
|
|
276
|
-
// names rather than re-routing via the router; concrete skills must be
|
|
277
|
-
// at the top level so Skill(skill="gsd-plan-phase") succeeds)
|
|
278
|
-
//
|
|
279
|
-
// FLAT (nested-scan behaviour unconfirmed → conservative):
|
|
280
|
-
// codex — developers.openai.com/codex/skills/
|
|
281
|
-
// copilot — docs.github.com/en/copilot/concepts/agents/about-agent-skills
|
|
282
|
-
// windsurf — docs.devin.ai/desktop/cascade/skills
|
|
283
|
-
// codebuddy — codebuddy.ai/docs/cli/skills
|
|
254
|
+
/** Lazy registry accessor — mirrors pattern from 5b/5c (runtime-homes.cts). */
|
|
255
|
+
function getRegistry() {
|
|
256
|
+
return _require('./capability-registry.cjs');
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
|
|
260
|
+
* matching builder function. Mirrors the hand-built calls in the old switch.
|
|
261
|
+
*/
|
|
262
|
+
function dispatchKindEntry(entry, runtime, configDir, scope) {
|
|
263
|
+
const { kind, destSubpath, prefix, nesting, converter } = entry;
|
|
264
|
+
const nested = nesting === 'nested';
|
|
265
|
+
switch (kind) {
|
|
266
|
+
case 'commands':
|
|
267
|
+
if (converter == null) {
|
|
268
|
+
return commandsKind(destSubpath, prefix, configDir);
|
|
269
|
+
}
|
|
270
|
+
return convertedCommandsKind(destSubpath, prefix, converter, configDir);
|
|
271
|
+
case 'agents':
|
|
272
|
+
return agentsKind(destSubpath, prefix, configDir);
|
|
273
|
+
case 'skills':
|
|
274
|
+
if (converter == null) {
|
|
275
|
+
throw new TypeError(`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`);
|
|
276
|
+
}
|
|
277
|
+
return skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope);
|
|
278
|
+
case 'kimi-agents':
|
|
279
|
+
return kimiAgentsKind(destSubpath, prefix, configDir);
|
|
280
|
+
default:
|
|
281
|
+
throw new TypeError(`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
284
|
/**
|
|
285
285
|
* Resolve the artifact layout for a given runtime and config directory.
|
|
286
|
+
*
|
|
287
|
+
* ADR-857 phase 5d: driven by the capability-registry artifactLayout descriptor
|
|
288
|
+
* instead of a hardcoded switch statement.
|
|
286
289
|
*/
|
|
287
290
|
function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
288
291
|
if (typeof configDir !== 'string' || configDir === '') {
|
|
@@ -294,106 +297,13 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
|
294
297
|
if (!ALLOWED_RUNTIMES.has(runtime)) {
|
|
295
298
|
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
|
296
299
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
kinds = [
|
|
302
|
-
commandsKind('commands/gsd', 'gsd-', configDir),
|
|
303
|
-
agentsKind('agents', 'gsd-', configDir),
|
|
304
|
-
];
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'claude', configDir)];
|
|
308
|
-
}
|
|
309
|
-
break;
|
|
310
|
-
case 'cursor':
|
|
311
|
-
// Cursor 1.6+ supports two artifact surfaces:
|
|
312
|
-
// 1. skills/gsd-<name>/SKILL.md — rich skills with frontmatter + adapter header
|
|
313
|
-
// 2. commands/gsd-<name>.md — plain markdown slash commands (no frontmatter)
|
|
314
|
-
// accessed via '/' in the Agent input (#785)
|
|
315
|
-
kinds = [
|
|
316
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir),
|
|
317
|
-
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir),
|
|
318
|
-
];
|
|
319
|
-
break;
|
|
320
|
-
case 'gemini':
|
|
321
|
-
kinds = [commandsKind('commands/gsd', 'gsd-', configDir)];
|
|
322
|
-
break;
|
|
323
|
-
case 'codex':
|
|
324
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodexSkill', 'codex', configDir)];
|
|
325
|
-
break;
|
|
326
|
-
case 'copilot':
|
|
327
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCopilotSkill', 'copilot', configDir)];
|
|
328
|
-
break;
|
|
329
|
-
case 'antigravity':
|
|
330
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAntigravitySkill', 'antigravity', configDir, true /* #69 nested */)];
|
|
331
|
-
break;
|
|
332
|
-
case 'windsurf':
|
|
333
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToWindsurfSkill', 'windsurf', configDir)];
|
|
334
|
-
break;
|
|
335
|
-
case 'augment':
|
|
336
|
-
kinds = [
|
|
337
|
-
commandsKind('commands', 'gsd-', configDir),
|
|
338
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir, true /* #69 nested */),
|
|
339
|
-
];
|
|
340
|
-
break;
|
|
341
|
-
case 'trae':
|
|
342
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir, true /* #69 nested */)];
|
|
343
|
-
break;
|
|
344
|
-
case 'qwen':
|
|
345
|
-
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'qwen', configDir, true /* #69 nested */)];
|
|
346
|
-
break;
|
|
347
|
-
case 'hermes':
|
|
348
|
-
// #947: restore canonical gsd- prefix — skills land at skills/gsd/gsd-<stem>/SKILL.md
|
|
349
|
-
// and dispatch as /gsd-<stem>, consistent with every other runtime.
|
|
350
|
-
// The skills/gsd/ category bucket (introduced by #2841) is retained.
|
|
351
|
-
// Prior bare-stem layout (prefix='') used by #3664 is reversed here.
|
|
352
|
-
kinds = [skillsKind('skills/gsd', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir, true /* #69 nested */)];
|
|
353
|
-
break;
|
|
354
|
-
case 'codebuddy':
|
|
355
|
-
// CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli):
|
|
356
|
-
// 1. commands/gsd-<name>.md — slash commands shown in the '/' menu (#789)
|
|
357
|
-
// 2. skills/gsd-<name>/SKILL.md — model-invocable skills, emitted with
|
|
358
|
-
// user-invocable:false so they stay OUT of '/' (the commands surface is
|
|
359
|
-
// the sole '/' entry point) — avoids a duplicated /gsd-* per workflow.
|
|
360
|
-
// Subagents (~/.codebuddy/agents/) are already emitted by the generic agents
|
|
361
|
-
// block in bin/install.js; MCP is excluded (gsd ships no MCP server).
|
|
362
|
-
kinds = [
|
|
363
|
-
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir),
|
|
364
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir),
|
|
365
|
-
];
|
|
366
|
-
break;
|
|
367
|
-
case 'cline':
|
|
368
|
-
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir, true /* #69 nested */)] : [];
|
|
369
|
-
break;
|
|
370
|
-
case 'kimi':
|
|
371
|
-
kinds = scope === 'global'
|
|
372
|
-
? [
|
|
373
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir),
|
|
374
|
-
kimiAgentsKind('agents', 'gsd', configDir),
|
|
375
|
-
]
|
|
376
|
-
: [];
|
|
377
|
-
break;
|
|
378
|
-
case 'opencode':
|
|
379
|
-
// OpenCode reads flat slash commands from command/ and on-demand skills
|
|
380
|
-
// from skills/<name>/SKILL.md (https://opencode.ai/docs/skills). Emit both.
|
|
381
|
-
kinds = [
|
|
382
|
-
commandsKind('command', 'gsd-', configDir),
|
|
383
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir),
|
|
384
|
-
];
|
|
385
|
-
break;
|
|
386
|
-
case 'kilo':
|
|
387
|
-
// Kilo derives from OpenCode and shares the skills/<name>/SKILL.md layout
|
|
388
|
-
// (https://kilo.ai/docs/customize/skills). Emit flat commands + skills.
|
|
389
|
-
kinds = [
|
|
390
|
-
commandsKind('command', 'gsd-', configDir),
|
|
391
|
-
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir),
|
|
392
|
-
];
|
|
393
|
-
break;
|
|
394
|
-
default:
|
|
395
|
-
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
|
300
|
+
const desc = getRegistry().runtimes[runtime]?.runtime?.artifactLayout;
|
|
301
|
+
if (!desc) {
|
|
302
|
+
// Runtime is in ALLOWED_RUNTIMES but has no descriptor — reproduce old default: throw.
|
|
303
|
+
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
|
396
304
|
}
|
|
305
|
+
const entries = desc[scope] ?? [];
|
|
306
|
+
const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope));
|
|
397
307
|
return { runtime, configDir, scope, kinds };
|
|
398
308
|
}
|
|
399
309
|
module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };
|