release-skill 0.9.16 → 0.9.17
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/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +10 -0
- package/CHANGELOG.md +30 -0
- package/INSTALL.md +57 -9
- package/INSTALL.zh-CN.md +44 -7
- package/README.md +75 -31
- package/README.zh-CN.md +60 -29
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1443 -672
- package/adapters/claude/schemas/release-plan.schema.json +1 -1
- package/adapters/claude/schemas/release-project.schema.json +1 -1
- package/adapters/claude/skills/release-finish/SKILL.md +14 -13
- package/adapters/claude/skills/release-help/SKILL.md +3 -3
- package/adapters/claude/skills/release-verify/SKILL.md +2 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1443 -672
- package/adapters/codex/schemas/release-plan.schema.json +1 -1
- package/adapters/codex/schemas/release-project.schema.json +1 -1
- package/adapters/codex/skills/release-finish/SKILL.md +14 -13
- package/adapters/codex/skills/release-help/SKILL.md +3 -3
- package/adapters/codex/skills/release-verify/SKILL.md +2 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1443 -672
- package/adapters/kimi/schemas/release-plan.schema.json +1 -1
- package/adapters/kimi/schemas/release-project.schema.json +1 -1
- package/adapters/kimi/skills/release-finish/SKILL.md +14 -13
- package/adapters/kimi/skills/release-help/SKILL.md +3 -3
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +10 -0
- package/adapters/qoder/bin/consumer-contract-vectors.json +110 -0
- package/adapters/qoder/bin/error-codes.json +161 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-contracts/LICENSE +202 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-contracts/NOTICE +10 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-engineering-kit/LICENSE +202 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-engineering-kit/NOTICE +10 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-engineering-kit/THIRD_PARTY_NOTICES +142 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-harness-node/LICENSE +202 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-harness-node/NOTICE +10 -0
- package/adapters/qoder/bin/foundation-legal/skill-family-harness-node/native-NOTICE +2 -0
- package/adapters/qoder/bin/foundation-resource-binding.json +1 -0
- package/adapters/qoder/bin/kernel-protocol.json +60 -0
- package/adapters/qoder/bin/license-texts/Apache-2.0.txt +201 -0
- package/adapters/qoder/bin/license-texts/MIT.txt +21 -0
- package/adapters/qoder/bin/prebuild-manifest.json +79 -0
- package/adapters/qoder/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/qoder/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/qoder/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/qoder/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/qoder/bin/registry.json +347 -0
- package/adapters/qoder/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/qoder/bin/release-skill.bundle.mjs +149446 -0
- package/adapters/qoder/bin/release-skill.mjs +54 -0
- package/adapters/qoder/bin/rules.json +104 -0
- package/adapters/qoder/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/qoder/data/hosts/claude/host-descriptor.json +45 -0
- package/adapters/qoder/data/hosts/codebuddy/host-descriptor.json +26 -0
- package/adapters/qoder/data/hosts/codex/host-descriptor.json +45 -0
- package/adapters/qoder/data/hosts/deepseek-harness/host-descriptor.json +23 -0
- package/adapters/qoder/data/hosts/kimi-code/host-descriptor.json +36 -0
- package/adapters/qoder/data/hosts/qoder/host-descriptor.json +26 -0
- package/adapters/qoder/data/hosts/registry.json +13 -0
- package/adapters/qoder/data/hosts/workbuddy/host-descriptor.json +34 -0
- package/adapters/qoder/data/licensing/registry.json +211 -0
- package/adapters/qoder/data/licensing/schema.json +207 -0
- package/adapters/qoder/native/safe-write/binding.gyp +41 -0
- package/adapters/qoder/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/qoder/native/safe-write/prebuilds.json +24 -0
- package/adapters/qoder/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/qoder/schemas/.render-manifest.json +41 -0
- package/adapters/qoder/schemas/approval-record.schema.json +115 -0
- package/adapters/qoder/schemas/artifact-lock.schema.json +111 -0
- package/adapters/qoder/schemas/artifact-plan.schema.json +52 -0
- package/adapters/qoder/schemas/artifact-policy.schema.json +76 -0
- package/adapters/qoder/schemas/evidence-event-v2.schema.json +164 -0
- package/adapters/qoder/schemas/evidence-event.schema.json +89 -0
- package/adapters/qoder/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/qoder/schemas/release-plan.schema.json +1996 -0
- package/adapters/qoder/schemas/release-project.schema.json +1613 -0
- package/adapters/qoder/schemas/release-run.schema.json +777 -0
- package/adapters/qoder/skills/release-assess/SKILL.md +60 -0
- package/adapters/qoder/skills/release-config/SKILL.md +150 -0
- package/adapters/qoder/skills/release-docs/SKILL.md +119 -0
- package/adapters/qoder/skills/release-finish/SKILL.md +141 -0
- package/adapters/qoder/skills/release-help/SKILL.md +153 -0
- package/adapters/qoder/skills/release-marketplace/SKILL.md +156 -0
- package/adapters/qoder/skills/release-prepare/SKILL.md +135 -0
- package/adapters/qoder/skills/release-publish/SKILL.md +91 -0
- package/adapters/qoder/skills/release-reconcile/SKILL.md +80 -0
- package/adapters/qoder/skills/release-setup/SKILL.md +131 -0
- package/adapters/qoder/skills/release-verify/SKILL.md +109 -0
- package/adapters/qoder/src/schemas/adapter-build-manifest.schema.json +41 -0
- package/adapters/qoder/src/schemas/adapter-peer-verification-request.schema.json +54 -0
- package/adapters/qoder/src/schemas/adapter-peer-verification-result.schema.json +54 -0
- package/adapters/qoder/src/schemas/adapter-source.schema.json +47 -0
- package/adapters/qoder/src/schemas/audit-baseline-pin.schema.json +163 -0
- package/adapters/qoder/src/schemas/declared-read-surface-result.schema.json +61 -0
- package/adapters/qoder/src/schemas/engineering-baseline.schema.json +77 -0
- package/adapters/qoder/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/qoder/src/schemas/filesystem-root-binding.schema.json +14 -0
- package/adapters/qoder/src/schemas/filesystem-tree-observation.schema.json +71 -0
- package/adapters/qoder/src/schemas/fixed-set-publication-manifest.schema.json +108 -0
- package/adapters/qoder/src/schemas/fixed-set-publication-receipt.schema.json +148 -0
- package/adapters/qoder/src/schemas/host-capability-fact.schema.json +31 -0
- package/adapters/qoder/src/schemas/host-descriptor.schema.json +137 -0
- package/adapters/qoder/src/schemas/host-operation-plan.schema.json +57 -0
- package/adapters/qoder/src/schemas/host-operation-receipt.schema.json +81 -0
- package/adapters/qoder/src/schemas/host-probe-result.schema.json +30 -0
- package/adapters/qoder/src/schemas/host-registry.schema.json +18 -0
- package/adapters/qoder/src/schemas/host-verification-request.schema.json +67 -0
- package/adapters/qoder/src/schemas/host-verification-result.schema.json +149 -0
- package/adapters/qoder/src/schemas/managed-file-lock.schema.json +77 -0
- package/adapters/qoder/src/schemas/migration-manifest.schema.json +412 -0
- package/adapters/qoder/src/schemas/observation-scope.schema.json +91 -0
- package/adapters/qoder/src/schemas/operation-request.schema.json +58 -0
- package/adapters/qoder/src/schemas/operation-result.schema.json +130 -0
- package/adapters/qoder/src/schemas/platform-difference-registry.schema.json +110 -0
- package/adapters/qoder/src/schemas/plugin-verification-request.schema.json +499 -0
- package/adapters/qoder/src/schemas/plugin-verification-result.schema.json +1217 -0
- package/adapters/qoder/src/schemas/profile-adoption-declaration.schema.json +111 -0
- package/adapters/qoder/src/schemas/profile-descriptor.schema.json +99 -0
- package/adapters/qoder/src/schemas/project-manifest.schema.json +67 -0
- package/adapters/qoder/src/schemas/project-profile.schema.json +25 -0
- package/adapters/qoder/src/schemas/public-boundary-declaration.schema.json +73 -0
- package/adapters/qoder/src/schemas/report-binding.schema.json +42 -0
- package/adapters/qoder/src/schemas/report-model.schema.json +308 -0
- package/adapters/qoder/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/qoder/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/qoder/src/schemas/source-authority-receipt.schema.json +31 -0
- package/adapters/qoder/src/schemas/state-event-envelope.schema.json +38 -0
- package/adapters/qoder/src/schemas/state-snapshot-metadata.schema.json +25 -0
- package/adapters/qoder/src/schemas/structured-scan-policy.schema.json +97 -0
- package/adapters/qoder/src/schemas/surface-scan-policy.schema.json +42 -0
- package/adapters/qoder/src/schemas/timeout-policy.schema.json +24 -0
- package/adapters/qoder/src/schemas/token-estimate-record.schema.json +75 -0
- package/adapters/qoder/src/schemas/token-estimate-result.schema.json +43 -0
- package/adapters/qoder/src/schemas/watchdog-termination-envelope.schema.json +156 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1443 -672
- package/adapters/workbuddy/schemas/release-plan.schema.json +1 -1
- package/adapters/workbuddy/schemas/release-project.schema.json +1 -1
- package/adapters/workbuddy/skills/release-finish/SKILL.md +14 -13
- package/adapters/workbuddy/skills/release-help/SKILL.md +3 -3
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -2
- package/bin/release-skill-cli.mjs +124 -6
- package/bin/release-skill.bundle.mjs +1443 -672
- package/package.json +4 -2
- package/platform-manifest.json +70 -4
- package/references/02-project-config.md +9 -2
- package/schemas/release-plan.schema.json +1 -1
- package/schemas/release-project.schema.json +1 -1
- package/skills/release-finish/SKILL.md +14 -13
- package/skills/release-help/SKILL.md +3 -3
- package/skills/release-verify/SKILL.md +2 -2
- package/skills-src/release-finish/SKILL.md +14 -13
- package/skills-src/release-help/SKILL.md +3 -3
- package/skills-src/release-verify/SKILL.md +2 -2
- package/src/commands/post-release-local.mjs +360 -51
- package/src/commands/verify-records.mjs +467 -0
- package/src/core/postpublish.mjs +1 -1
- package/src/core/run.mjs +1 -1
- package/src/producers/build-adapters.mjs +36 -13
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, offline verification of caller-supplied release records.
|
|
3
|
+
*
|
|
4
|
+
* This module never discovers records or performs I/O. The CLI layer supplies
|
|
5
|
+
* the exact bytes and source labels selected by the caller.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { digestBytes } from 'skill-family-harness-node';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
computeApprovalDigest,
|
|
12
|
+
validateApproval,
|
|
13
|
+
validateApprovalRecordSchema,
|
|
14
|
+
validateApprovalTimeWindow,
|
|
15
|
+
} from '../core/approval.mjs';
|
|
16
|
+
import { isMarketplaceAction, isRemoteWriteAction } from '../core/checkpoints.mjs';
|
|
17
|
+
import { computePlanDigest, validatePlan } from '../core/plan.mjs';
|
|
18
|
+
import {
|
|
19
|
+
computeRunDigest,
|
|
20
|
+
validateRun,
|
|
21
|
+
validateRunCheckpointMapping,
|
|
22
|
+
validateRunPlanDigest,
|
|
23
|
+
validateSourceRunEdge,
|
|
24
|
+
} from '../core/run.mjs';
|
|
25
|
+
|
|
26
|
+
export const VERIFY_RECORDS_EXIT_CODES = Object.freeze({
|
|
27
|
+
CONSISTENT: 0,
|
|
28
|
+
CONTRADICTED: 1,
|
|
29
|
+
INSUFFICIENT: 2,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SUPPORTED_PLAN_VERSIONS = new Set([1, 2, 3]);
|
|
33
|
+
const SUPPORTED_RUN_COMMANDS = new Set(['publish', 'reconcile', 'verify']);
|
|
34
|
+
const SUPPORTED_TERMINAL_STATUSES = new Set(['PARTIAL', 'PUBLISHED', 'VERIFIED']);
|
|
35
|
+
const FINDING_ORDER = Object.freeze([
|
|
36
|
+
'INPUT_MISSING',
|
|
37
|
+
'INPUT_DAMAGED',
|
|
38
|
+
'FORMAT_UNSUPPORTED',
|
|
39
|
+
'PLAN_DIGEST_MISMATCH',
|
|
40
|
+
'APPROVAL_DIGEST_MISMATCH',
|
|
41
|
+
'RUN_DIGEST_MISMATCH',
|
|
42
|
+
'IDENTITY_MISMATCH',
|
|
43
|
+
'PLAN_BINDING_MISMATCH',
|
|
44
|
+
'APPROVAL_ACTION_MISMATCH',
|
|
45
|
+
'RUN_LINEAGE_MISMATCH',
|
|
46
|
+
'TRANSITION_MISMATCH',
|
|
47
|
+
'CHECKPOINT_MISMATCH',
|
|
48
|
+
'HISTORICAL_TIME_MISSING',
|
|
49
|
+
'HISTORICAL_TIME_OUTSIDE_WINDOW',
|
|
50
|
+
]);
|
|
51
|
+
const FINDING_RANK = new Map(FINDING_ORDER.map((code, index) => [code, index]));
|
|
52
|
+
|
|
53
|
+
function sourceLabel(source) {
|
|
54
|
+
if (typeof source !== 'string' || source.length === 0) return null;
|
|
55
|
+
return source.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseInput(input, role, addFinding) {
|
|
59
|
+
const summary = { role, source: sourceLabel(input?.source) };
|
|
60
|
+
if (!input || input.bytes === undefined || input.bytes === null) {
|
|
61
|
+
addFinding('INPUT_MISSING', role, `required ${role} input was not provided`, 'INSUFFICIENT');
|
|
62
|
+
return { summary, digest: null, document: null, bytes: null, usable: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let bytes;
|
|
66
|
+
try {
|
|
67
|
+
bytes = Buffer.isBuffer(input.bytes)
|
|
68
|
+
? Buffer.from(input.bytes)
|
|
69
|
+
: Buffer.from(input.bytes);
|
|
70
|
+
} catch {
|
|
71
|
+
addFinding('INPUT_DAMAGED', role, `${role} input is not byte-compatible`);
|
|
72
|
+
return { summary, digest: null, document: null, bytes: null, usable: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const bytesSha256 = digestBytes(bytes);
|
|
76
|
+
try {
|
|
77
|
+
const document = JSON.parse(bytes.toString('utf8'));
|
|
78
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
79
|
+
addFinding('INPUT_DAMAGED', role, `${role} input must be a JSON object`);
|
|
80
|
+
return {
|
|
81
|
+
summary,
|
|
82
|
+
digest: { bytesSha256, carriedDomainDigest: null, recomputedDomainDigest: null },
|
|
83
|
+
document: null,
|
|
84
|
+
bytes,
|
|
85
|
+
usable: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
summary,
|
|
90
|
+
digest: { bytesSha256, carriedDomainDigest: null, recomputedDomainDigest: null },
|
|
91
|
+
document,
|
|
92
|
+
bytes,
|
|
93
|
+
usable: true,
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
addFinding('INPUT_DAMAGED', role, `${role} input is not valid JSON`);
|
|
97
|
+
return {
|
|
98
|
+
summary,
|
|
99
|
+
digest: { bytesSha256, carriedDomainDigest: null, recomputedDomainDigest: null },
|
|
100
|
+
document: null,
|
|
101
|
+
bytes,
|
|
102
|
+
usable: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sameStringSet(left, right) {
|
|
108
|
+
return JSON.stringify([...new Set(left)].sort()) === JSON.stringify([...new Set(right)].sort());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function commandStatusIsValid(run) {
|
|
112
|
+
if (run.command === 'publish' || run.command === 'reconcile') {
|
|
113
|
+
return run.status === 'PARTIAL' || run.status === 'PUBLISHED';
|
|
114
|
+
}
|
|
115
|
+
return run.command === 'verify' && run.status === 'VERIFIED';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkpointStatusIsValid(run) {
|
|
119
|
+
if (run.status === 'VERIFIED') {
|
|
120
|
+
return run.checkpoints.every((checkpoint) => ['succeeded', 'skipped'].includes(checkpoint.status));
|
|
121
|
+
}
|
|
122
|
+
if (run.status === 'PUBLISHED') {
|
|
123
|
+
return run.checkpoints.every((checkpoint) => (
|
|
124
|
+
isRemoteWriteAction(checkpoint.actionType)
|
|
125
|
+
? ['succeeded', 'skipped'].includes(checkpoint.status)
|
|
126
|
+
: isMarketplaceAction(checkpoint.actionType)
|
|
127
|
+
? ['succeeded', 'skipped', 'deferred', 'failed'].includes(checkpoint.status)
|
|
128
|
+
: true
|
|
129
|
+
));
|
|
130
|
+
}
|
|
131
|
+
if (run.status === 'PARTIAL') {
|
|
132
|
+
return run.checkpoints.some((checkpoint) => !['succeeded', 'skipped', 'deferred'].includes(checkpoint.status));
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Verify one explicitly supplied record set without filesystem, network, or
|
|
139
|
+
* process access.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} request
|
|
142
|
+
* @param {{bytes: Buffer|string, source?: string}} request.plan
|
|
143
|
+
* @param {{bytes: Buffer|string, source?: string}} request.approval
|
|
144
|
+
* @param {{bytes: Buffer|string, source?: string}} request.targetRun
|
|
145
|
+
* @param {Array<{bytes: Buffer|string, source?: string}>} [request.sourceRuns]
|
|
146
|
+
* @param {string} request.unitId
|
|
147
|
+
* @param {string} request.targetVersion
|
|
148
|
+
*/
|
|
149
|
+
export function verifyReleaseRecords(request = {}) {
|
|
150
|
+
const findings = [];
|
|
151
|
+
const addFinding = (code, role, message, outcome = 'CONTRADICTED') => {
|
|
152
|
+
findings.push({ code, role, message, _outcome: outcome });
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const planInput = parseInput(request.plan, 'plan', addFinding);
|
|
156
|
+
const approvalInput = parseInput(request.approval, 'approval', addFinding);
|
|
157
|
+
const targetInput = parseInput(request.targetRun, 'targetRun', addFinding);
|
|
158
|
+
const sourceInputs = Array.isArray(request.sourceRuns)
|
|
159
|
+
? request.sourceRuns.map((input, index) => parseInput(input, `sourceRun[${index}]`, addFinding))
|
|
160
|
+
: [];
|
|
161
|
+
|
|
162
|
+
const plan = planInput.document;
|
|
163
|
+
const approval = approvalInput.document;
|
|
164
|
+
const targetRun = targetInput.document;
|
|
165
|
+
|
|
166
|
+
let planSupported = false;
|
|
167
|
+
if (planInput.usable) {
|
|
168
|
+
if (!SUPPORTED_PLAN_VERSIONS.has(plan?.planVersion)) {
|
|
169
|
+
addFinding('FORMAT_UNSUPPORTED', 'plan', `unsupported release plan version: ${String(plan?.planVersion)}`, 'INSUFFICIENT');
|
|
170
|
+
} else {
|
|
171
|
+
planSupported = true;
|
|
172
|
+
try {
|
|
173
|
+
validatePlan(plan);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
addFinding('INPUT_DAMAGED', 'plan', error.message);
|
|
176
|
+
planSupported = false;
|
|
177
|
+
}
|
|
178
|
+
const recomputed = computePlanDigest(plan);
|
|
179
|
+
planInput.digest.carriedDomainDigest = plan?.digest ?? null;
|
|
180
|
+
planInput.digest.recomputedDomainDigest = recomputed;
|
|
181
|
+
if (!plan?.digest || plan.digest !== recomputed) {
|
|
182
|
+
addFinding('PLAN_DIGEST_MISMATCH', 'plan', 'plan digest does not match its binding content');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let approvalSupported = false;
|
|
188
|
+
let approvalDigest = null;
|
|
189
|
+
if (approvalInput.usable) {
|
|
190
|
+
approvalDigest = computeApprovalDigest(approvalInput.bytes);
|
|
191
|
+
approvalInput.digest.recomputedDomainDigest = approvalDigest;
|
|
192
|
+
try {
|
|
193
|
+
validateApprovalRecordSchema(approval);
|
|
194
|
+
approvalSupported = true;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
addFinding('INPUT_DAMAGED', 'approval', error.message);
|
|
197
|
+
}
|
|
198
|
+
if (approvalSupported) {
|
|
199
|
+
try {
|
|
200
|
+
validateApprovalTimeWindow(approval, {
|
|
201
|
+
clock: () => approval.approvedAt,
|
|
202
|
+
requireUnexpired: false,
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
addFinding('INPUT_DAMAGED', 'approval', error.message);
|
|
206
|
+
approvalSupported = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const runEntries = [
|
|
212
|
+
{ input: targetInput, role: 'targetRun', isTarget: true },
|
|
213
|
+
...sourceInputs.map((input, index) => ({ input, role: `sourceRun[${index}]`, isTarget: false })),
|
|
214
|
+
];
|
|
215
|
+
const usableRuns = [];
|
|
216
|
+
let trustedTargetTerminalStatus = null;
|
|
217
|
+
for (const entry of runEntries) {
|
|
218
|
+
const run = entry.input.document;
|
|
219
|
+
if (!entry.input.usable) continue;
|
|
220
|
+
entry.input.digest.carriedDomainDigest = run?.runDigest ?? null;
|
|
221
|
+
entry.input.digest.recomputedDomainDigest = computeRunDigest(run);
|
|
222
|
+
|
|
223
|
+
if (!SUPPORTED_RUN_COMMANDS.has(run?.command) || !SUPPORTED_TERMINAL_STATUSES.has(run?.status)) {
|
|
224
|
+
addFinding(
|
|
225
|
+
'FORMAT_UNSUPPORTED',
|
|
226
|
+
entry.role,
|
|
227
|
+
`unsupported release run command/status: ${String(run?.command)}/${String(run?.status)}`,
|
|
228
|
+
'INSUFFICIENT',
|
|
229
|
+
);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { runDigest: _runDigest, ...unsignedRun } = run;
|
|
234
|
+
try {
|
|
235
|
+
validateRun(unsignedRun);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
addFinding('INPUT_DAMAGED', entry.role, error.message);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const digestMatches = Boolean(run.runDigest)
|
|
242
|
+
&& run.runDigest === entry.input.digest.recomputedDomainDigest;
|
|
243
|
+
const transitionIsValid = commandStatusIsValid(run);
|
|
244
|
+
const terminalCheckpointsAreValid = checkpointStatusIsValid(run);
|
|
245
|
+
if (!digestMatches) {
|
|
246
|
+
addFinding('RUN_DIGEST_MISMATCH', entry.role, 'run digest does not match its content');
|
|
247
|
+
}
|
|
248
|
+
if (!transitionIsValid) {
|
|
249
|
+
addFinding('TRANSITION_MISMATCH', entry.role, `run command ${run.command} cannot terminate as ${run.status}`);
|
|
250
|
+
}
|
|
251
|
+
if (!terminalCheckpointsAreValid) {
|
|
252
|
+
addFinding('CHECKPOINT_MISMATCH', entry.role, `checkpoint results do not support terminal status ${run.status}`);
|
|
253
|
+
}
|
|
254
|
+
if (entry.isTarget && digestMatches && transitionIsValid && terminalCheckpointsAreValid) {
|
|
255
|
+
trustedTargetTerminalStatus = run.status;
|
|
256
|
+
}
|
|
257
|
+
usableRuns.push({ ...entry, run });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (planSupported && typeof request.unitId === 'string' && typeof request.targetVersion === 'string') {
|
|
261
|
+
const expectedUnit = (plan.units ?? []).find((unit) => unit.id === request.unitId);
|
|
262
|
+
if (!expectedUnit || expectedUnit.targetVersion !== request.targetVersion) {
|
|
263
|
+
addFinding('IDENTITY_MISMATCH', 'plan', 'expected release unit and version do not match the plan');
|
|
264
|
+
}
|
|
265
|
+
} else if (typeof request.unitId !== 'string' || typeof request.targetVersion !== 'string') {
|
|
266
|
+
addFinding('INPUT_MISSING', 'identity', 'unitId and targetVersion are required', 'INSUFFICIENT');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (planSupported && approvalSupported) {
|
|
270
|
+
const planDigest = computePlanDigest(plan);
|
|
271
|
+
if (approval.planDigest !== planDigest) {
|
|
272
|
+
addFinding('PLAN_BINDING_MISMATCH', 'approval', 'approval planDigest does not match the supplied plan');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const expectedUnitVersions = Object.fromEntries((plan.units ?? []).map((unit) => [unit.id, unit.targetVersion]));
|
|
276
|
+
const approvedUnitVersions = approval.unitVersions
|
|
277
|
+
?? ((plan.units ?? []).length === 1 ? { [plan.units[0].id]: approval.targetVersion } : {});
|
|
278
|
+
const identityMatches = sameStringSet(Object.keys(expectedUnitVersions), Object.keys(approvedUnitVersions))
|
|
279
|
+
&& Object.entries(expectedUnitVersions).every(([unitId, version]) => approvedUnitVersions[unitId] === version)
|
|
280
|
+
&& (approval.targetVersion === undefined
|
|
281
|
+
|| new Set(Object.values(expectedUnitVersions)).size !== 1
|
|
282
|
+
|| approval.targetVersion === Object.values(expectedUnitVersions)[0]);
|
|
283
|
+
if (!identityMatches) {
|
|
284
|
+
addFinding('IDENTITY_MISMATCH', 'approval', 'approval unit/version identity does not match the plan');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const planActions = (plan.externalActions ?? []).map((action) => action.id);
|
|
288
|
+
if (!sameStringSet(approval.approvedActions ?? [], planActions)) {
|
|
289
|
+
addFinding('APPROVAL_ACTION_MISMATCH', 'approval', 'approved actions do not exactly match the plan actions');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
validateApproval(plan, approval, {
|
|
294
|
+
clock: () => approval.approvedAt,
|
|
295
|
+
requireUnexpired: false,
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const alreadyRepresented = (
|
|
299
|
+
(/planDigest/i.test(error.message)
|
|
300
|
+
&& findings.some((finding) => finding.role === 'approval' && finding.code === 'PLAN_BINDING_MISMATCH'))
|
|
301
|
+
|| (/version|unitVersions/i.test(error.message)
|
|
302
|
+
&& findings.some((finding) => finding.role === 'approval' && finding.code === 'IDENTITY_MISMATCH'))
|
|
303
|
+
|| (/approved|action/i.test(error.message)
|
|
304
|
+
&& findings.some((finding) => finding.role === 'approval' && finding.code === 'APPROVAL_ACTION_MISMATCH'))
|
|
305
|
+
);
|
|
306
|
+
if (!alreadyRepresented) {
|
|
307
|
+
addFinding('INPUT_DAMAGED', 'approval', error.message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (planSupported) {
|
|
313
|
+
for (const entry of usableRuns) {
|
|
314
|
+
try {
|
|
315
|
+
validateRunPlanDigest(entry.run, plan);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
addFinding('PLAN_BINDING_MISMATCH', entry.role, error.message);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
validateRunCheckpointMapping(entry.run, plan.externalActions ?? []);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
addFinding('CHECKPOINT_MISMATCH', entry.role, error.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (approvalDigest) {
|
|
328
|
+
for (const entry of usableRuns) {
|
|
329
|
+
if (!entry.run.approvalDigest) {
|
|
330
|
+
addFinding('INPUT_MISSING', entry.role, 'run does not carry the approval digest needed to bind the supplied approval', 'INSUFFICIENT');
|
|
331
|
+
} else if (entry.run.approvalDigest !== approvalDigest) {
|
|
332
|
+
addFinding('APPROVAL_DIGEST_MISMATCH', entry.role, 'run approvalDigest does not match the supplied approval bytes');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const supportingRuns = usableRuns.filter((entry) => !entry.isTarget);
|
|
338
|
+
const chain = [];
|
|
339
|
+
let lineageComplete = false;
|
|
340
|
+
if (targetRun && usableRuns.some((entry) => entry.isTarget)) {
|
|
341
|
+
let current = usableRuns.find((entry) => entry.isTarget);
|
|
342
|
+
const visited = new Set();
|
|
343
|
+
for (let depth = 0; depth <= 16 && current; depth += 1) {
|
|
344
|
+
chain.push(current.run);
|
|
345
|
+
const visitKey = `${current.run.runId}:${current.run.runDigest}`;
|
|
346
|
+
if (visited.has(visitKey)) {
|
|
347
|
+
addFinding('RUN_LINEAGE_MISMATCH', current.role, 'release run lineage contains a cycle');
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
visited.add(visitKey);
|
|
351
|
+
|
|
352
|
+
if (current.run.command === 'publish') {
|
|
353
|
+
lineageComplete = true;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (!current.run.sourceRunId || !current.run.sourceRunDigest || !current.run.sourceRunPath) {
|
|
357
|
+
addFinding('INPUT_MISSING', current.role, 'run is missing complete source-run lineage fields', 'INSUFFICIENT');
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const exactParent = supportingRuns.find((candidate) => (
|
|
361
|
+
candidate.run.runId === current.run.sourceRunId
|
|
362
|
+
&& candidate.run.runDigest === current.run.sourceRunDigest
|
|
363
|
+
));
|
|
364
|
+
if (!exactParent) {
|
|
365
|
+
const sameId = supportingRuns.find((candidate) => candidate.run.runId === current.run.sourceRunId);
|
|
366
|
+
if (sameId) {
|
|
367
|
+
addFinding('RUN_LINEAGE_MISMATCH', current.role, 'source run id is present but its digest does not match');
|
|
368
|
+
} else {
|
|
369
|
+
addFinding('INPUT_MISSING', current.role, 'explicit source run required by lineage was not supplied', 'INSUFFICIENT');
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
validateSourceRunEdge(current.run, exactParent.run);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
addFinding('TRANSITION_MISMATCH', current.role, error.message);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
current = exactParent;
|
|
380
|
+
}
|
|
381
|
+
if (!lineageComplete && chain.length > 16) {
|
|
382
|
+
addFinding('RUN_LINEAGE_MISMATCH', 'targetRun', 'release run lineage exceeds maximum depth');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const entry of usableRuns) {
|
|
387
|
+
const run = entry.run;
|
|
388
|
+
if (run.stateSequence === undefined) {
|
|
389
|
+
if (run.previousStateDigest !== undefined) {
|
|
390
|
+
addFinding('RUN_LINEAGE_MISMATCH', entry.role, 'non-state run claims a previous state digest');
|
|
391
|
+
}
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (run.stateSequence === 0) {
|
|
395
|
+
if (run.previousStateDigest !== undefined) {
|
|
396
|
+
addFinding('RUN_LINEAGE_MISMATCH', entry.role, 'initial state run claims a previous state digest');
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const predecessor = supportingRuns.find((candidate) => (
|
|
401
|
+
candidate.run.runId === run.runId
|
|
402
|
+
&& candidate.run.stateSequence === run.stateSequence - 1
|
|
403
|
+
));
|
|
404
|
+
if (!predecessor) {
|
|
405
|
+
addFinding('INPUT_MISSING', entry.role, 'explicit predecessor state snapshot was not supplied', 'INSUFFICIENT');
|
|
406
|
+
} else if (
|
|
407
|
+
predecessor.run.runDigest !== run.previousStateDigest
|
|
408
|
+
|| predecessor.run.command !== run.command
|
|
409
|
+
|| predecessor.run.planDigest !== run.planDigest
|
|
410
|
+
) {
|
|
411
|
+
addFinding('RUN_LINEAGE_MISMATCH', entry.role, 'state predecessor identity or digest does not match');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (approvalSupported) {
|
|
416
|
+
const executionRun = [...chain].reverse().find((run) => (
|
|
417
|
+
['publish', 'reconcile'].includes(run.command)
|
|
418
|
+
&& (run.checkpoints ?? []).some((checkpoint) => isRemoteWriteAction(checkpoint.actionType))
|
|
419
|
+
));
|
|
420
|
+
if (!lineageComplete || !executionRun?.startedAt || !executionRun?.finishedAt) {
|
|
421
|
+
addFinding('HISTORICAL_TIME_MISSING', 'approval', 'historical publish execution interval cannot be proven', 'INSUFFICIENT');
|
|
422
|
+
} else {
|
|
423
|
+
const approvedAt = Date.parse(approval.approvedAt);
|
|
424
|
+
const expiresAt = Date.parse(approval.expiresAt);
|
|
425
|
+
const startedAt = Date.parse(executionRun.startedAt);
|
|
426
|
+
const finishedAt = Date.parse(executionRun.finishedAt);
|
|
427
|
+
if (
|
|
428
|
+
!Number.isFinite(startedAt)
|
|
429
|
+
|| !Number.isFinite(finishedAt)
|
|
430
|
+
|| startedAt < approvedAt
|
|
431
|
+
|| finishedAt > expiresAt
|
|
432
|
+
|| finishedAt < startedAt
|
|
433
|
+
) {
|
|
434
|
+
addFinding('HISTORICAL_TIME_OUTSIDE_WINDOW', 'approval', 'publish execution interval is outside the approval window');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
findings.sort((left, right) => (
|
|
440
|
+
(FINDING_RANK.get(left.code) ?? 999) - (FINDING_RANK.get(right.code) ?? 999)
|
|
441
|
+
|| left.role.localeCompare(right.role)
|
|
442
|
+
|| left.message.localeCompare(right.message)
|
|
443
|
+
));
|
|
444
|
+
const hasContradiction = findings.some((finding) => finding._outcome === 'CONTRADICTED');
|
|
445
|
+
const hasInsufficient = findings.some((finding) => finding._outcome === 'INSUFFICIENT');
|
|
446
|
+
const status = hasContradiction ? 'CONTRADICTED' : hasInsufficient ? 'INSUFFICIENT' : 'CONSISTENT';
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
status,
|
|
450
|
+
unitId: typeof request.unitId === 'string' ? request.unitId : null,
|
|
451
|
+
targetVersion: typeof request.targetVersion === 'string' ? request.targetVersion : null,
|
|
452
|
+
historicalTerminalStatus: trustedTargetTerminalStatus,
|
|
453
|
+
inputs: {
|
|
454
|
+
plan: planInput.summary,
|
|
455
|
+
approval: approvalInput.summary,
|
|
456
|
+
targetRun: targetInput.summary,
|
|
457
|
+
sourceRuns: sourceInputs.map((input) => input.summary),
|
|
458
|
+
},
|
|
459
|
+
digests: {
|
|
460
|
+
plan: planInput.digest,
|
|
461
|
+
approval: approvalInput.digest,
|
|
462
|
+
targetRun: targetInput.digest,
|
|
463
|
+
sourceRuns: sourceInputs.map((input) => input.digest),
|
|
464
|
+
},
|
|
465
|
+
findings: findings.map(({ _outcome, ...finding }) => finding),
|
|
466
|
+
};
|
|
467
|
+
}
|
package/src/core/postpublish.mjs
CHANGED
|
@@ -42,7 +42,7 @@ const SAFE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
|
42
42
|
|
|
43
43
|
/** Branch pattern (leading alphanumeric blocks option-like names). */
|
|
44
44
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
45
|
-
const LOCAL_HOSTS = new Set(['claude', 'codex', 'kimi', 'codebuddy', 'workbuddy']);
|
|
45
|
+
const LOCAL_HOSTS = new Set(['claude', 'codex', 'kimi', 'codebuddy', 'workbuddy', 'qoder']);
|
|
46
46
|
const HUB_HOST_RE = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/u;
|
|
47
47
|
const HUB_REF_RE = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/u;
|
|
48
48
|
|
package/src/core/run.mjs
CHANGED
|
@@ -464,7 +464,7 @@ async function validateStatePredecessorChain(run, runPath, options = {}) {
|
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
function validateSourceRunEdge(child, parent) {
|
|
467
|
+
export function validateSourceRunEdge(child, parent) {
|
|
468
468
|
if (child.command === 'reconcile') {
|
|
469
469
|
if (!['publish', 'reconcile'].includes(parent.command) || parent.status !== 'PARTIAL') {
|
|
470
470
|
throw new ReleaseError(
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reads skill metadata from skills-src/ and plugin.json templates,
|
|
5
5
|
* generates self-contained adapter directories for each platform
|
|
6
|
-
* (claude, codex, kimi, and the build-only
|
|
6
|
+
* (claude, codex, kimi, workbuddy, and the build-only qoder adapter).
|
|
7
7
|
*
|
|
8
8
|
* Each adapter root contains:
|
|
9
|
-
* - Plugin manifest (.claude-plugin/, .codex-plugin/, .kimi-plugin/,
|
|
10
|
-
* .codebuddy-plugin/)
|
|
9
|
+
* - Plugin manifest (.claude-plugin/, .codex-plugin/, .kimi-plugin/,
|
|
10
|
+
* .codebuddy-plugin/, or .qoder-plugin/)
|
|
11
11
|
* - Skills with host-specific root resolution
|
|
12
12
|
* - bin/release-skill.mjs (wrapper) + bin/release-skill.bundle.mjs (self-contained bundle)
|
|
13
13
|
* - schemas/ (JSON Schema files)
|
|
@@ -227,16 +227,20 @@ const REGISTRY_PLATFORMS = PLATFORM_REGISTRY.map((platform) => ({
|
|
|
227
227
|
* until then a build-only entry lets the build emit an installable adapter
|
|
228
228
|
* tree without fabricating an unverified install protocol.
|
|
229
229
|
*
|
|
230
|
-
*
|
|
231
|
-
* descriptor. workbuddy (the CodeBuddy/WorkBuddy plugin, manifest
|
|
230
|
+
* workbuddy (the CodeBuddy/WorkBuddy plugin, manifest
|
|
232
231
|
* `.codebuddy-plugin/plugin.json`, `${CODEBUDDY_PLUGIN_ROOT}` expanded inline
|
|
233
232
|
* in skill content) moved into the registry as the `codebuddy` platform with
|
|
234
233
|
* `buildAdapter.name = 'workbuddy'`, so its adapter tree is still produced —
|
|
235
|
-
* byte-for-byte unchanged — via REGISTRY_PLATFORMS.
|
|
236
|
-
*
|
|
237
|
-
*
|
|
234
|
+
* byte-for-byte unchanged — via REGISTRY_PLATFORMS. Qoder remains build-only:
|
|
235
|
+
* its installable projection is public, while release plans intentionally do
|
|
236
|
+
* not gain a qoder distribution type or publish checkpoint.
|
|
238
237
|
*/
|
|
239
|
-
export const BUILD_ONLY_ADAPTERS = Object.freeze([
|
|
238
|
+
export const BUILD_ONLY_ADAPTERS = Object.freeze([Object.freeze({
|
|
239
|
+
name: 'qoder',
|
|
240
|
+
pluginDirName: '.qoder-plugin',
|
|
241
|
+
templateFileName: 'plugin.json',
|
|
242
|
+
hasMarketplace: false,
|
|
243
|
+
})]);
|
|
240
244
|
|
|
241
245
|
export const PLATFORMS = Object.freeze([...REGISTRY_PLATFORMS, ...BUILD_ONLY_ADAPTERS]);
|
|
242
246
|
|
|
@@ -344,6 +348,20 @@ const KIMI_PREAMBLE = `\
|
|
|
344
348
|
>\n\
|
|
345
349
|
`;
|
|
346
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Qoder exposes the absolute location of the skill it loaded. The generated
|
|
353
|
+
* adapter uses that host-provided location to derive its self-contained root;
|
|
354
|
+
* Qoder does not expand Claude's plugin-root placeholder.
|
|
355
|
+
*/
|
|
356
|
+
const QODER_PREAMBLE = `\
|
|
357
|
+
> **Qoder 安装入口解析协议**:在调用 CLI 前,Agent 必须从宿主当前已加载技能的元数据中取得本 \`SKILL.md\` 的实际绝对路径,并将该字面量记为 \`SKILL_FILE\`。\n\
|
|
358
|
+
> \`SKILL_FILE\` 不是环境变量;禁止从工作目录、可执行搜索路径、源码仓库或 shell 调用上下文猜测。若宿主未提供该绝对路径,立即停止并报告安装定位失败。\n\
|
|
359
|
+
> 对 \`SKILL_FILE\` 执行 \`realpath\`,取其目录向上两级得到 \`PLUGIN_ROOT\`;校验真实技能路径匹配 \`PLUGIN_ROOT/skills/*/SKILL.md\` 且仍位于插件根内(路径包含检查)。\n\
|
|
360
|
+
> 令 \`RELEASE_SKILL_ENTRY=PLUGIN_ROOT/bin/release-skill.mjs\`,对入口执行 \`realpath\` containment、\`lstat\` 非符号链接且为普通文件校验。\n\
|
|
361
|
+
> 每一次 shell 工具调用都必须在同一个调用中用上述已验证绝对值设置 \`RELEASE_SKILL_ENTRY\`,然后执行 \`node "$RELEASE_SKILL_ENTRY" ...\`;不得依赖前一次 shell 的变量。\n\
|
|
362
|
+
>\n\
|
|
363
|
+
`;
|
|
364
|
+
|
|
347
365
|
/**
|
|
348
366
|
* Render SKILL.md content for a specific platform.
|
|
349
367
|
*
|
|
@@ -351,13 +369,14 @@ const KIMI_PREAMBLE = `\
|
|
|
351
369
|
* - Codex: replaces the Claude-only path with $RELEASE_SKILL_ENTRY,
|
|
352
370
|
* and prepends the path resolution protocol preamble.
|
|
353
371
|
* - Kimi: same substitution as Codex, with the Kimi Code preamble.
|
|
372
|
+
* - Qoder: same substitution, with a host-loaded-path preamble.
|
|
354
373
|
* - WorkBuddy: pure variable substitution (${CLAUDE_PLUGIN_ROOT} ->
|
|
355
374
|
* ${CODEBUDDY_PLUGIN_ROOT}); CodeBuddy expands the variable inline in
|
|
356
375
|
* skill content (official plugin reference), the same mechanism Claude
|
|
357
376
|
* uses, so no entry-resolution preamble is needed.
|
|
358
377
|
*
|
|
359
378
|
* @param {string} content - Canonical SKILL.md content.
|
|
360
|
-
* @param {string} platformName - 'claude', 'codex', 'kimi', or '
|
|
379
|
+
* @param {string} platformName - 'claude', 'codex', 'kimi', 'workbuddy', or 'qoder'.
|
|
361
380
|
* @returns {string}
|
|
362
381
|
*/
|
|
363
382
|
function renderSkillForPlatform(content, platformName) {
|
|
@@ -367,9 +386,13 @@ function renderSkillForPlatform(content, platformName) {
|
|
|
367
386
|
return content.replaceAll('${CLAUDE_PLUGIN_ROOT}', '${CODEBUDDY_PLUGIN_ROOT}');
|
|
368
387
|
}
|
|
369
388
|
|
|
370
|
-
if (platformName === 'codex' || platformName === 'kimi') {
|
|
371
|
-
const hostLabel =
|
|
372
|
-
const preamble =
|
|
389
|
+
if (platformName === 'codex' || platformName === 'kimi' || platformName === 'qoder') {
|
|
390
|
+
const hostLabel = { codex: 'Codex', kimi: 'Kimi', qoder: 'Qoder' }[platformName];
|
|
391
|
+
const preamble = {
|
|
392
|
+
codex: CODEX_PREAMBLE,
|
|
393
|
+
kimi: KIMI_PREAMBLE,
|
|
394
|
+
qoder: QODER_PREAMBLE,
|
|
395
|
+
}[platformName];
|
|
373
396
|
let rendered = content.replaceAll(
|
|
374
397
|
'${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs',
|
|
375
398
|
'$RELEASE_SKILL_LOCAL_FINISH_ENTRY',
|