devlyn-cli 4.2.25 → 4.3.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/bin/codex-projection.js +129 -0
- package/bin/devlyn.js +522 -0
- package/bin/engines.js +40 -0
- package/bin/harness-activation.js +426 -0
- package/bin/harness-status.js +171 -0
- package/bin/instructions.js +13 -3
- package/bin/release.js +381 -0
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/adapters/omp.md +3 -0
- package/config/skills/_shared/harness-store.py +201 -0
- package/config/skills/_shared/runtime-principles.md +12 -2
- package/config/skills/_shared/warm-build-dirs.py +154 -83
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +156 -15
- package/package.json +8 -1
package/bin/instructions.js
CHANGED
|
@@ -13,12 +13,21 @@ const digest = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
|
13
13
|
|
|
14
14
|
class InstructionError extends Error {}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
function managedBody(template) {
|
|
17
|
+
return 'Project-specific instructions outside this managed block take precedence over these defaults.\n\n'
|
|
18
18
|
+ normalize(template).replace(/\n?$/, '\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function managedBlock(template, eol = '\n') {
|
|
22
|
+
const body = managedBody(template);
|
|
19
23
|
return `${BEGIN} sha256=${digest(body)} -->\n${body}${END}\n`.replace(/\n/g, eol);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
function sessionDefaults(template) {
|
|
27
|
+
return 'The following Devlyn instructions are defaults for this session. Applicable explicit user instructions and native owner/project instructions take precedence over these defaults. This delegation applies only to the Devlyn defaults below; it does not change other engine instructions or permissions.\n\n'
|
|
28
|
+
+ normalize(template);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
function matchedEnd(text, start, template) {
|
|
23
32
|
let end = start;
|
|
24
33
|
for (let n = 0; n < template.length && end < text.length; n++, end++) {
|
|
@@ -335,4 +344,5 @@ function updateInstructions(name, initial = '', withBlock = true, write = true,
|
|
|
335
344
|
return true;
|
|
336
345
|
}
|
|
337
346
|
|
|
338
|
-
module.exports = { updateInstructions, InstructionError, instructionParagraphs, holdsDevlynDefaults, importsAgentsMd
|
|
347
|
+
module.exports = { updateInstructions, InstructionError, instructionParagraphs, holdsDevlynDefaults, importsAgentsMd,
|
|
348
|
+
managedBody, managedBlock, sessionDefaults };
|
package/bin/release.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { parseArgs } = require('util');
|
|
6
|
+
const { managedBody, managedBlock, sessionDefaults } = require('./instructions');
|
|
7
|
+
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
const CORE = ['devlyn-ideate', 'devlyn-engines', '_shared'];
|
|
10
|
+
const DEFAULTS = 'config/skills/_shared/runtime-principles.md';
|
|
11
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
12
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
13
|
+
|
|
14
|
+
class HarnessError extends Error {
|
|
15
|
+
constructor(code, params = {}, exitCode = 1) {
|
|
16
|
+
super(`${code}: ${JSON.stringify(params)}`);
|
|
17
|
+
this.reason = { code, params };
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function fail(code, params, exitCode) { throw new HarnessError(code, params, exitCode); }
|
|
22
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
23
|
+
function canonical(value) {
|
|
24
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
25
|
+
if (value && typeof value === 'object') {
|
|
26
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(value);
|
|
29
|
+
}
|
|
30
|
+
function manifestDigest(manifest) {
|
|
31
|
+
const { digest, ...body } = manifest;
|
|
32
|
+
return sha256(canonical(body));
|
|
33
|
+
}
|
|
34
|
+
const jsonBytes = (value) => Buffer.from(JSON.stringify(value, null, 2) + '\n');
|
|
35
|
+
|
|
36
|
+
// POSIX manifest names must also be unambiguous on Windows and macOS volumes.
|
|
37
|
+
function validatePath(name) {
|
|
38
|
+
if (typeof name !== 'string' || !name || /[\\\x00-\x1f\x7f:<>"|?*]/.test(name)
|
|
39
|
+
|| path.posix.isAbsolute(name) || path.win32.isAbsolute(name)
|
|
40
|
+
|| name.split('/').some((part) => !part || part === '.' || part === '..' || /[. ]$/.test(part)
|
|
41
|
+
|| /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part))) {
|
|
42
|
+
fail('release-unsafe-path', { path: String(name) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function validatePaths(names) {
|
|
46
|
+
const seen = new Map();
|
|
47
|
+
const files = new Set();
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
validatePath(name);
|
|
50
|
+
if (files.has(name)) fail('release-duplicate-path', { path: name });
|
|
51
|
+
files.add(name);
|
|
52
|
+
const parts = name.split('/');
|
|
53
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
54
|
+
const prefix = parts.slice(0, i).join('/');
|
|
55
|
+
const folded = prefix.normalize('NFC').toUpperCase().toLowerCase().toUpperCase();
|
|
56
|
+
if (seen.has(folded) && seen.get(folded) !== prefix) fail('release-case-collision', { path: name });
|
|
57
|
+
seen.set(folded, prefix);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
const parts = name.split('/');
|
|
62
|
+
for (let i = 1; i < parts.length; i++) {
|
|
63
|
+
if (files.has(parts.slice(0, i).join('/'))) fail('release-path-conflict', { path: name });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function regular(file, directory = false) {
|
|
68
|
+
const stat = fs.lstatSync(file);
|
|
69
|
+
if (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1) {
|
|
70
|
+
fail('release-unsafe-entry', { path: file });
|
|
71
|
+
}
|
|
72
|
+
return stat;
|
|
73
|
+
}
|
|
74
|
+
function physicalRoot(root) {
|
|
75
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) fail('invalid-arguments', { argument: 'absolute path required' }, 2);
|
|
76
|
+
regular(root, true);
|
|
77
|
+
return fs.realpathSync(root);
|
|
78
|
+
}
|
|
79
|
+
function safeFile(root, relative) {
|
|
80
|
+
validatePath(relative);
|
|
81
|
+
const parts = relative.split('/');
|
|
82
|
+
for (let i = 1; i < parts.length; i++) regular(path.join(root, ...parts.slice(0, i)), true);
|
|
83
|
+
const file = path.join(root, ...parts);
|
|
84
|
+
regular(file);
|
|
85
|
+
return file;
|
|
86
|
+
}
|
|
87
|
+
function readJson(file) {
|
|
88
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof SyntaxError) fail('invalid-json', { path: file });
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function treeFiles(root, relative = '', npmInstall = false) {
|
|
95
|
+
regular(path.join(root, relative), true);
|
|
96
|
+
const files = [];
|
|
97
|
+
for (const name of fs.readdirSync(path.join(root, relative)).sort()) {
|
|
98
|
+
const rel = relative ? `${relative}/${name}` : name;
|
|
99
|
+
validatePath(rel);
|
|
100
|
+
const stat = fs.lstatSync(path.join(root, rel));
|
|
101
|
+
if (stat.isDirectory()) {
|
|
102
|
+
// npm generates these launch shims after unpacking. Mutable install sources
|
|
103
|
+
// may contain them; immutable releases never do, and capture never copies them.
|
|
104
|
+
if (npmInstall && rel === 'node_modules/.bin') continue;
|
|
105
|
+
files.push(...treeFiles(root, rel, npmInstall));
|
|
106
|
+
}
|
|
107
|
+
else { regular(path.join(root, rel)); files.push(rel); }
|
|
108
|
+
}
|
|
109
|
+
validatePaths(files);
|
|
110
|
+
return files.sort();
|
|
111
|
+
}
|
|
112
|
+
function packageMetadata(root) {
|
|
113
|
+
const pkg = readJson(safeFile(root, 'package.json'));
|
|
114
|
+
if (pkg.name !== 'devlyn-cli' || !VERSION.test(pkg.version) || typeof pkg.engines?.node !== 'string') {
|
|
115
|
+
fail('release-schema', { field: 'package.json' }, 3);
|
|
116
|
+
}
|
|
117
|
+
return pkg;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function generatedArtifacts(root) {
|
|
121
|
+
const pkg = packageMetadata(root);
|
|
122
|
+
const template = fs.readFileSync(safeFile(root, 'CLAUDE.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
const agents = fs.readFileSync(safeFile(root, 'AGENTS.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
124
|
+
if (template !== agents || /<!-- devlyn:instructions:(?:begin|end)/.test(template)) {
|
|
125
|
+
fail('release-template-mismatch', {});
|
|
126
|
+
}
|
|
127
|
+
const outputs = new Map();
|
|
128
|
+
const put = (name, bytes, executable = false) => outputs.set(name, { bytes: Buffer.from(bytes), executable });
|
|
129
|
+
put(DEFAULTS, sessionDefaults(template));
|
|
130
|
+
put('harness/CLAUDE.md', managedBlock(template));
|
|
131
|
+
put('harness/AGENTS.md', managedBlock(template));
|
|
132
|
+
const plugin = { name: 'devlyn', version: pkg.version, description: 'Devlyn harness skills', skills: './skills/' };
|
|
133
|
+
put('native/claude/.claude-plugin/plugin.json', jsonBytes(plugin));
|
|
134
|
+
put('native/codex/plugins/devlyn/.codex-plugin/plugin.json', jsonBytes(plugin));
|
|
135
|
+
put('native/codex/.agents/plugins/marketplace.json', jsonBytes({
|
|
136
|
+
name: `devlyn-harness-v${pkg.version.replace(/\./g, '-')}`,
|
|
137
|
+
plugins: [{ name: 'devlyn', source: { source: 'local', path: './plugins/devlyn' } }],
|
|
138
|
+
}));
|
|
139
|
+
for (const skill of CORE) {
|
|
140
|
+
const source = `config/skills/${skill}`;
|
|
141
|
+
for (const relative of treeFiles(path.join(root, source))) {
|
|
142
|
+
// Python bytecode and OS metadata are not release inputs.
|
|
143
|
+
if (relative.split('/').includes('__pycache__') || /(?:^|\/)(?:\.DS_Store|Thumbs\.db)$|\.pyc$/.test(relative)) continue;
|
|
144
|
+
const name = `${source}/${relative}`;
|
|
145
|
+
const file = safeFile(root, name);
|
|
146
|
+
const bytes = outputs.get(name)?.bytes || fs.readFileSync(file);
|
|
147
|
+
const executable = name === DEFAULTS ? false : Boolean(fs.statSync(file).mode & 0o111);
|
|
148
|
+
for (const mirror of ['native/claude', 'native/codex/plugins/devlyn']) put(`${mirror}/skills/${skill}/${relative}`, bytes, executable);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return outputs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkGenerated(root) {
|
|
155
|
+
const expected = generatedArtifacts(root);
|
|
156
|
+
for (const [name, { bytes, executable }] of expected) {
|
|
157
|
+
const file = safeFile(root, name);
|
|
158
|
+
if (!fs.readFileSync(file).equals(bytes) || (process.platform !== 'win32' && Boolean(fs.statSync(file).mode & 0o111) !== executable)) {
|
|
159
|
+
fail('release-generated-mismatch', { path: name });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const actual = ['harness', 'native'].flatMap((dir) => treeFiles(path.join(root, dir)).map((name) => `${dir}/${name}`));
|
|
163
|
+
if (actual.some((name) => !expected.has(name))) fail('release-generated-mismatch', { path: 'unlisted generated file' });
|
|
164
|
+
return expected.size;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function metadata(root) {
|
|
168
|
+
const pkg = packageMetadata(root);
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1, package: 'devlyn-cli', version: pkg.version,
|
|
171
|
+
principles: { defaults: DEFAULTS, memory: { claude: 'harness/CLAUDE.md', agents: 'harness/AGENTS.md' },
|
|
172
|
+
sourceBlockSha256: sha256(managedBody(fs.readFileSync(safeFile(root, 'CLAUDE.md'), 'utf8'))) },
|
|
173
|
+
skills: { root: 'config/skills', names: CORE.slice(0, 2), support: '_shared',
|
|
174
|
+
claudePlugin: 'native/claude', codexMarketplace: 'native/codex' },
|
|
175
|
+
stateCompat: {
|
|
176
|
+
loopManifest: { read: [1, 1], write: 1 }, addRecord: { read: [1, 1], write: 1 },
|
|
177
|
+
taskPacket: { inspect: [1, 2], execute: [1, 2], write: 2 },
|
|
178
|
+
queueBinding: { read: [1, 1], write: 1 },
|
|
179
|
+
submission: { read: [1, 1], write: 1 }, review: { read: [1, 1], write: 1 },
|
|
180
|
+
acceptanceResult: { read: [1, 1], write: 1 }, invocationReceipt: { read: [1, 1], write: 1 },
|
|
181
|
+
legacyFormats: ['completion-receipt-unversioned', 'direct-acceptance-unversioned',
|
|
182
|
+
'expected-contract-unversioned', 'engines-config-unversioned', 'queue-markdown-inspect-only'],
|
|
183
|
+
},
|
|
184
|
+
requires: { node: pkg.engines.node, python: '>=3.11', git: true, windowsGitBash: true, githubDelivery: 'gh' },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeArtifact(root, name, bytes, executable) {
|
|
189
|
+
const parts = name.split('/');
|
|
190
|
+
for (let i = 1; i < parts.length; i++) {
|
|
191
|
+
const dir = path.join(root, ...parts.slice(0, i));
|
|
192
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
|
193
|
+
regular(dir, true);
|
|
194
|
+
}
|
|
195
|
+
const file = path.join(root, name);
|
|
196
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })) regular(file);
|
|
197
|
+
fs.writeFileSync(file, bytes);
|
|
198
|
+
fs.chmodSync(file, executable ? 0o755 : 0o644);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildRelease(root = PACKAGE_ROOT) {
|
|
202
|
+
root = physicalRoot(root);
|
|
203
|
+
// npm omits links from its inventory; reject them before that omission can hide an input.
|
|
204
|
+
for (const dir of ['bin', 'config', 'optional-skills']) treeFiles(path.join(root, dir));
|
|
205
|
+
const outputs = generatedArtifacts(root);
|
|
206
|
+
// Generated roots are package-owned. Validate before removing stale mirror files.
|
|
207
|
+
for (const dir of ['harness', 'native']) {
|
|
208
|
+
if (fs.lstatSync(path.join(root, dir), { throwIfNoEntry: false })) {
|
|
209
|
+
treeFiles(path.join(root, dir));
|
|
210
|
+
fs.rmSync(path.join(root, dir), { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [name, { bytes, executable }] of outputs) writeArtifact(root, name, bytes, executable);
|
|
214
|
+
// npm's own inventory includes the exact bundled dependency closure and pack exclusions.
|
|
215
|
+
const npmCli = process.env.npm_execpath || (process.platform === 'win32'
|
|
216
|
+
? path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js') : null);
|
|
217
|
+
const result = spawnSync(npmCli ? process.execPath : 'npm', [
|
|
218
|
+
...(npmCli ? [npmCli] : []), 'pack', '--dry-run', '--ignore-scripts', '--json',
|
|
219
|
+
], { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
220
|
+
if (result.error || result.status !== 0) fail('release-pack-failed', { detail: result.error?.message || result.stderr.trim() });
|
|
221
|
+
const packed = JSON.parse(result.stdout)[0];
|
|
222
|
+
const names = packed.files.map((file) => file.path).filter((name) => name !== 'release.json').sort();
|
|
223
|
+
validatePaths(names);
|
|
224
|
+
const pkg = packageMetadata(root);
|
|
225
|
+
for (const dependency of pkg.bundleDependencies) {
|
|
226
|
+
if (!names.includes(`node_modules/${dependency}/package.json`)) fail('release-missing-dependency', { dependency });
|
|
227
|
+
}
|
|
228
|
+
const modes = new Map(packed.files.map((file) => [file.path, Boolean(file.mode & 0o111)]));
|
|
229
|
+
const manifest = { ...metadata(root), files: names.map((name) => ({
|
|
230
|
+
path: name, sha256: sha256(fs.readFileSync(safeFile(root, name))), executable: modes.get(name),
|
|
231
|
+
})) };
|
|
232
|
+
manifest.digest = { algorithm: 'sha256', value: manifestDigest(manifest) };
|
|
233
|
+
writeArtifact(root, 'release.json', jsonBytes(manifest), false);
|
|
234
|
+
return { manifestPath: path.join(root, 'release.json'), version: manifest.version,
|
|
235
|
+
digest: manifest.digest.value, fileCount: manifest.files.length };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// npm runs prepack in the source checkout and leaves its outputs there; postpack removes them, so a
|
|
239
|
+
// checkout never carries a release.json that would make its own skills read as a release.
|
|
240
|
+
function cleanGenerated(root = PACKAGE_ROOT) {
|
|
241
|
+
root = physicalRoot(root);
|
|
242
|
+
for (const name of ['harness', 'native', 'release.json']) {
|
|
243
|
+
const file = path.join(root, name);
|
|
244
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
245
|
+
if (!stat) continue;
|
|
246
|
+
if (stat.isSymbolicLink() || !(stat.isDirectory() || stat.isFile())) fail('release-unsafe-entry', { path: file });
|
|
247
|
+
fs.rmSync(file, { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function checkNpmShims(root, files) {
|
|
252
|
+
const dir = path.join(root, 'node_modules/.bin');
|
|
253
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })) return;
|
|
254
|
+
regular(dir, true);
|
|
255
|
+
const names = new Set(files.map((file) => file.path));
|
|
256
|
+
const targets = new Map();
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
if (!/^node_modules\/(?:@[^/]+\/)?[^/]+\/package\.json$/.test(file.path)) continue;
|
|
259
|
+
const pkg = readJson(safeFile(root, file.path));
|
|
260
|
+
const bins = typeof pkg.bin === 'string' ? { [pkg.name.split('/').at(-1)]: pkg.bin } : pkg.bin || {};
|
|
261
|
+
for (const [name, relative] of Object.entries(bins)) {
|
|
262
|
+
validatePath(name);
|
|
263
|
+
if (name.includes('/') || typeof relative !== 'string') fail('release-schema', { field: 'dependency bin' }, 3);
|
|
264
|
+
const target = path.posix.normalize(`${path.posix.dirname(file.path)}/${relative}`);
|
|
265
|
+
validatePath(target);
|
|
266
|
+
if (!names.has(target)) fail('release-file-list-mismatch', { path: target });
|
|
267
|
+
for (const shim of [name, `${name}.cmd`, `${name}.ps1`]) {
|
|
268
|
+
if (!targets.has(shim)) targets.set(shim, new Set());
|
|
269
|
+
targets.get(shim).add(path.join(root, target));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const name of fs.readdirSync(dir)) {
|
|
274
|
+
const file = path.join(dir, name);
|
|
275
|
+
const stat = fs.lstatSync(file);
|
|
276
|
+
if (!targets.has(name)) fail('release-file-list-mismatch', { path: `node_modules/.bin/${name}` });
|
|
277
|
+
if (stat.isSymbolicLink()) {
|
|
278
|
+
if (!targets.get(name).has(path.resolve(dir, fs.readlinkSync(file)))) fail('release-unsafe-entry', { path: file });
|
|
279
|
+
} else regular(file); // Windows shims are regular files; never execute or copy them.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function verifyRelease(root, { npmInstall = false } = {}) {
|
|
284
|
+
root = physicalRoot(root);
|
|
285
|
+
const manifest = readJson(safeFile(root, 'release.json'));
|
|
286
|
+
if (!manifest || manifest.schemaVersion !== 1 || manifest.package !== 'devlyn-cli'
|
|
287
|
+
|| !VERSION.test(manifest.version) || manifest.digest?.algorithm !== 'sha256'
|
|
288
|
+
|| !SHA256.test(manifest.digest?.value) || !Array.isArray(manifest.files)
|
|
289
|
+
|| !manifest.files.every((f) => f && typeof f.path === 'string' && SHA256.test(f.sha256) && typeof f.executable === 'boolean')) {
|
|
290
|
+
fail('release-schema', { path: 'release.json' }, 3);
|
|
291
|
+
}
|
|
292
|
+
const names = manifest.files.map((file) => file.path);
|
|
293
|
+
validatePaths([...names, 'release.json']);
|
|
294
|
+
if (canonical(names) !== canonical([...names].sort())) fail('release-schema', { field: 'files order' }, 3);
|
|
295
|
+
if (manifestDigest(manifest) !== manifest.digest.value) fail('release-digest-mismatch', { path: 'release.json' });
|
|
296
|
+
const actual = treeFiles(root, '', npmInstall).filter((name) => name !== 'release.json');
|
|
297
|
+
if (canonical(actual) !== canonical(names)) fail('release-file-list-mismatch', {});
|
|
298
|
+
for (const file of manifest.files) {
|
|
299
|
+
const full = safeFile(root, file.path);
|
|
300
|
+
if (sha256(fs.readFileSync(full)) !== file.sha256) fail('release-file-digest-mismatch', { path: file.path });
|
|
301
|
+
// Windows has no POSIX execute bit; its declared mode is retained in the digest.
|
|
302
|
+
if (process.platform !== 'win32' && Boolean(fs.statSync(full).mode & 0o111) !== file.executable) {
|
|
303
|
+
fail('release-mode-mismatch', { path: file.path });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (npmInstall) checkNpmShims(root, manifest.files);
|
|
307
|
+
const { files, digest, ...declared } = manifest;
|
|
308
|
+
if (canonical(declared) !== canonical(metadata(root))) fail('release-schema', { field: 'metadata' }, 3);
|
|
309
|
+
checkGenerated(root);
|
|
310
|
+
return { release: { package: 'devlyn-cli', version: manifest.version, digest: digest.value, root }, valid: true };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function materializeRelease(source, store) {
|
|
314
|
+
const verified = verifyRelease(source, { npmInstall: true }).release;
|
|
315
|
+
if (!path.isAbsolute(store)) fail('invalid-arguments', { argument: 'absolute store required' }, 2);
|
|
316
|
+
// Check every existing store component before mkdir; never write through a store link.
|
|
317
|
+
const missing = [];
|
|
318
|
+
let parent = path.resolve(store);
|
|
319
|
+
while (!fs.lstatSync(parent, { throwIfNoEntry: false })) { missing.push(parent); parent = path.dirname(parent); }
|
|
320
|
+
regular(parent, true);
|
|
321
|
+
for (let dir = parent; dir !== path.dirname(dir); dir = path.dirname(dir)) regular(dir, true);
|
|
322
|
+
for (const dir of missing.reverse()) { fs.mkdirSync(dir, { recursive: true }); regular(dir, true); }
|
|
323
|
+
store = physicalRoot(store);
|
|
324
|
+
const packageDir = path.join(store, 'devlyn-cli');
|
|
325
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
326
|
+
regular(packageDir, true);
|
|
327
|
+
const target = path.join(packageDir, verified.version);
|
|
328
|
+
const existing = () => {
|
|
329
|
+
const release = verifyRelease(target).release;
|
|
330
|
+
if (release.digest !== verified.digest) fail('release-version-conflict', { version: release.version }, 3);
|
|
331
|
+
return { release, created: false };
|
|
332
|
+
};
|
|
333
|
+
if (fs.lstatSync(target, { throwIfNoEntry: false })) return existing();
|
|
334
|
+
const temp = fs.mkdtempSync(path.join(packageDir, `.${verified.version}-`));
|
|
335
|
+
try {
|
|
336
|
+
const manifest = readJson(safeFile(verified.root, 'release.json'));
|
|
337
|
+
for (const file of [...manifest.files, { path: 'release.json', executable: false }]) {
|
|
338
|
+
writeArtifact(temp, file.path, fs.readFileSync(safeFile(verified.root, file.path)), file.executable);
|
|
339
|
+
}
|
|
340
|
+
const captured = verifyRelease(temp).release;
|
|
341
|
+
if (captured.digest !== verified.digest) fail('release-source-changed', {}, 3);
|
|
342
|
+
try { fs.renameSync(temp, target); }
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code) && fs.existsSync(target)) return existing();
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
return { release: { ...captured, root: target }, created: true };
|
|
348
|
+
} finally {
|
|
349
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function options(args, definitions) {
|
|
354
|
+
try { return parseArgs({ args, options: { json: { type: 'boolean' }, ...definitions }, strict: true }).values; }
|
|
355
|
+
catch (error) { fail('invalid-arguments', { detail: error.message }, 2); }
|
|
356
|
+
}
|
|
357
|
+
function releaseCommand(args) {
|
|
358
|
+
const opts = options(args, Object.fromEntries(['build', 'verify', 'materialize', 'root', 'source', 'store']
|
|
359
|
+
.map((key) => [key, { type: ['build', 'verify', 'materialize'].includes(key) ? 'boolean' : 'string' }])));
|
|
360
|
+
const actions = ['build', 'verify', 'materialize'].filter((key) => opts[key]);
|
|
361
|
+
const allowed = { build: ['build', 'json'], verify: ['verify', 'root', 'json'], materialize: ['materialize', 'source', 'store', 'json'] };
|
|
362
|
+
if (actions.length !== 1 || Object.keys(opts).some((key) => !allowed[actions[0]].includes(key))
|
|
363
|
+
|| (opts.verify && !opts.root) || (opts.materialize && (!opts.source || !opts.store))) fail('invalid-arguments', {}, 2);
|
|
364
|
+
if (opts.build) return buildRelease();
|
|
365
|
+
if (opts.verify) return verifyRelease(opts.root);
|
|
366
|
+
return materializeRelease(opts.source, opts.store);
|
|
367
|
+
}
|
|
368
|
+
function runJson(action) {
|
|
369
|
+
try { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, data: action() }) + '\n'); }
|
|
370
|
+
catch (error) {
|
|
371
|
+
const reason = error instanceof HarnessError ? error.reason
|
|
372
|
+
: { code: 'operation-failed', params: { detail: error.message, ...(error.code ? { systemCode: error.code } : {}) } };
|
|
373
|
+
process.stderr.write(`${reason.code}: ${JSON.stringify(reason.params)}\n`);
|
|
374
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: reason }) + '\n');
|
|
375
|
+
process.exitCode = error instanceof HarnessError ? error.exitCode : 1;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = { buildRelease, cleanGenerated, verifyRelease, materializeRelease, checkGenerated,
|
|
380
|
+
sha256, manifestDigest, physicalRoot, HarnessError, fail,
|
|
381
|
+
options, releaseCommand, runJson };
|
package/bin/skill-history.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"161cb4f1b27a9f81ae1e96af6ff596399c8936578a11d601108a89cbdeea241d",
|
|
19
19
|
"196afa14871099a9afc4e484b00e8d8ccd928dba79db95a6a8776f58c8e78c39",
|
|
20
20
|
"1c1e5e44ff704b1b8791fabcad79a4158d8cd2f024569c1685ec3b185a98fbcc",
|
|
21
|
+
"20ac702b463f3eaa07fd6686d5209aed7993773504e35896fc732f7e653ec6cd",
|
|
21
22
|
"20bfec10dcec9a06701827a6a39faf3495f970f659f1e941cbfb62e880a0649c",
|
|
22
23
|
"20e11efa7b27dcd30b5550ba5e0e2e07df9ac522a76445a594803a0e29b2e13d",
|
|
23
24
|
"21723943c52573124ce3c74e9090a8ae7d7c9ecdeb78fdfa52b89c45ff1b3374",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"3be2ec1243eb28f779b170e469dfb65555a92dc08b3d927b8973cbd49308125d",
|
|
43
44
|
"3c58989be2f943896810ef709fb115171420a918dfaadc43ea47940fa8ebc7ed",
|
|
44
45
|
"3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045",
|
|
46
|
+
"3cb043970432281f3c2c3c7ec813c4721221ca18ff227e94ef204f2ef2178044",
|
|
45
47
|
"3cd398df22e666fdd2e0f253c5a7464bca62bceefe3737d28a5ef95b75cb3a33",
|
|
46
48
|
"3e39fbe29d0bd607f4ad940f4ca40f9abf82d468e418d0362fb0fd4fa6812475",
|
|
47
49
|
"3e5135877e733f1dcff3b747444228f5eece7cb047956c87a3af02a485cfaad9",
|
|
@@ -90,6 +92,7 @@
|
|
|
90
92
|
"7a0f6c692bd1f74759d44f1a145899c157e1787635db546ea8a192461421e353",
|
|
91
93
|
"7ca704d818bf9c044fed59fdd4107bd0463f9b0514d44729003e3e52aa796353",
|
|
92
94
|
"7e915cc7307af694e326c7f75cf831d187edd965f38d870323cd50f0f01a564b",
|
|
95
|
+
"7f5f9f318f465b01c573fa521922a218e0973056a53053d262ca6ffb84dda145",
|
|
93
96
|
"800543bb9d1f8b90a7c077f072aa45364a0136d3c4e9b7acddcdbc30d63eb0c8",
|
|
94
97
|
"823e497e01870123d4e73c8dac7472e161b5ffa2f6181ddcba71fcb829096948",
|
|
95
98
|
"83b6d37fcacc086a09b9c3eddfff2263bfef23f8fabf8d8833a7c9f9fb6186c5",
|
|
@@ -147,6 +150,7 @@
|
|
|
147
150
|
"db867d1fd98058bb20e41e942abd95f08fbaa9465928025f69f0a0308fa71514",
|
|
148
151
|
"dd0bfdfd2db0b5f9ddef75d1b9ce3247698739fc551d292366dccd506fef6a36",
|
|
149
152
|
"ddb0c8dac58e95298330ab54e4d1fe4a108c968d9af713f267b4f8f4c6808507",
|
|
153
|
+
"de4f504265e960cb8d81752e9ae922f664790ee026b4454f1b07252d1a6282a4",
|
|
150
154
|
"e0a305cf3bf47c8bc357195d4d50b0f6177069e870c33040c258c93edae68646",
|
|
151
155
|
"e115205a744fef918a59d1b3a601df09a8b6df1d3e60613b9e8b87240929f8d8",
|
|
152
156
|
"e41013abf5e17445ef83a86974ac1de77beaa61e484e1916577ecc64bb34c220",
|
|
@@ -354,6 +358,7 @@
|
|
|
354
358
|
"d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e",
|
|
355
359
|
"db8d921aabf0d04811b55465a3757c62c4b39631aebaf6eecc994ca241cfbe56",
|
|
356
360
|
"e25da746dc37b2e82c658a28d9481fd69741151045683eee2119582ddaf731cd",
|
|
361
|
+
"e47a2ce766ae81fa0870e0a8dfbfe737311de69ce620e92217abbe4c532e3c8b",
|
|
357
362
|
"e929f76c15349f1b9bc1e84e0a3e9e48e916168d79270e0f4a36ea8eab9b5388",
|
|
358
363
|
"f2243cc4150e225961825f6097b0cf38166c291dcf58127516ceee841f7ed4c8",
|
|
359
364
|
"fa536198d539a3c85f54dda74d0d78716ac6e6849e733bdadecc8605511eac37",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Release identity and queue resource checks. Readers never write or create locks.
|
|
2
|
+
|
|
3
|
+
The release verifier is CLI-1's bin/release.js, not a second manifest verifier.
|
|
4
|
+
A Codex projection has .devlyn-harness-projection.json at its plugin root:
|
|
5
|
+
{schemaVersion: 1, release: ReleaseRef, source: "native/codex/plugins/devlyn"}.
|
|
6
|
+
Its complete regular-file tree (except that marker) must match that release subtree.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
PROJECTION = ".devlyn-harness-projection.json"
|
|
19
|
+
CODEX_SOURCE = "native/codex/plugins/devlyn"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HarnessError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def require(condition, message):
|
|
27
|
+
if not condition:
|
|
28
|
+
raise HarnessError(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_json(path):
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(Path(path).read_bytes())
|
|
34
|
+
except (OSError, ValueError) as exc:
|
|
35
|
+
raise HarnessError(f"{path}: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def file_identity(path):
|
|
39
|
+
path = Path(path)
|
|
40
|
+
info = path.lstat()
|
|
41
|
+
require(path.is_file() and not path.is_symlink() and info.st_nlink == 1, f"not a regular resource: {path}")
|
|
42
|
+
return {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def release_ref(value):
|
|
46
|
+
require(isinstance(value, dict) and set(value) == {"package", "version", "digest", "root"}
|
|
47
|
+
and value["package"] == "devlyn-cli"
|
|
48
|
+
and all(isinstance(value[k], str) and value[k] and "\0" not in value[k] for k in ("version", "digest", "root"))
|
|
49
|
+
and re.fullmatch(r"[a-f0-9]{64}", value["digest"]) and Path(value["root"]).is_absolute(), "malformed release reference")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_release(root):
|
|
54
|
+
root = Path(root)
|
|
55
|
+
script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2]));"
|
|
56
|
+
result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
|
|
57
|
+
require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
|
|
58
|
+
try:
|
|
59
|
+
release = release_ref(json.loads(result.stdout)["data"]["release"])
|
|
60
|
+
except (KeyError, ValueError, TypeError) as exc:
|
|
61
|
+
raise HarnessError(f"invalid release verifier result: {root}: {exc}") from exc
|
|
62
|
+
manifest = read_json(root / "release.json")
|
|
63
|
+
defaults = file_identity(root / manifest["principles"]["defaults"])
|
|
64
|
+
return {"schemaVersion": 1, "release": release, "defaults": defaults}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def walk_error(error):
|
|
68
|
+
raise error # os.walk otherwise silently skips unreadable projection directories.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve(skills):
|
|
72
|
+
"""Resolve only this invoking skill's real ancestry; never search another install."""
|
|
73
|
+
skill = (Path(skills) / "devlyn-ideate").resolve()
|
|
74
|
+
for parent in (skill, *skill.parents):
|
|
75
|
+
marker = parent / PROJECTION
|
|
76
|
+
if marker.exists() or marker.is_symlink():
|
|
77
|
+
file_identity(marker)
|
|
78
|
+
projection = read_json(marker)
|
|
79
|
+
require(isinstance(projection, dict) and set(projection) == {"schemaVersion", "release", "source"}
|
|
80
|
+
and type(projection["schemaVersion"]) is int and projection["schemaVersion"] == 1
|
|
81
|
+
and projection["source"] == CODEX_SOURCE, f"malformed projection: {marker}")
|
|
82
|
+
ref = release_ref(projection["release"])
|
|
83
|
+
binding = verify_release(ref["root"])
|
|
84
|
+
require(binding["release"] == ref, f"projection release changed: {marker}")
|
|
85
|
+
source = Path(ref["root"]) / CODEX_SOURCE
|
|
86
|
+
require(skill == parent / "skills/devlyn-ideate", f"unexpected projection skill: {skill}")
|
|
87
|
+
expected = {p.relative_to(source) for p in source.rglob("*") if p.is_file()}
|
|
88
|
+
actual = set()
|
|
89
|
+
for directory, dirs, files in os.walk(parent, onerror=walk_error):
|
|
90
|
+
for name in dirs:
|
|
91
|
+
require(not (Path(directory) / name).is_symlink(), f"projection directory link: {directory}/{name}")
|
|
92
|
+
for name in files:
|
|
93
|
+
file = Path(directory) / name
|
|
94
|
+
if file == marker:
|
|
95
|
+
continue
|
|
96
|
+
rel = file.relative_to(parent)
|
|
97
|
+
actual.add(rel)
|
|
98
|
+
require(rel in expected and file_identity(file)["sha256"] == file_identity(source / rel)["sha256"]
|
|
99
|
+
and (os.name == "nt" or bool(file.stat().st_mode & 0o111) == bool((source / rel).stat().st_mode & 0o111)),
|
|
100
|
+
f"projection resource changed: {file}")
|
|
101
|
+
require(actual == expected, f"projection file list changed: {parent}")
|
|
102
|
+
return binding
|
|
103
|
+
manifest = parent / "release.json"
|
|
104
|
+
if (manifest.exists() or manifest.is_symlink()) and skill in [parent / p / "devlyn-ideate" for p in
|
|
105
|
+
("config/skills", "native/claude/skills", CODEX_SOURCE + "/skills")]:
|
|
106
|
+
return verify_release(parent)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_binding(common):
|
|
111
|
+
path = Path(common) / "devlyn-loops/harness.json"
|
|
112
|
+
if not path.exists() and not path.is_symlink():
|
|
113
|
+
return None
|
|
114
|
+
file_identity(path)
|
|
115
|
+
binding = read_json(path)
|
|
116
|
+
require(isinstance(binding, dict) and set(binding) == {"schemaVersion", "release", "defaults"}
|
|
117
|
+
and type(binding["schemaVersion"]) is int and binding["schemaVersion"] == 1, f"incompatible queue binding: {path}")
|
|
118
|
+
release_ref(binding["release"])
|
|
119
|
+
resource(binding["defaults"])
|
|
120
|
+
return binding
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resource(value):
|
|
124
|
+
require(isinstance(value, dict) and set(value) == {"path", "sha256"}
|
|
125
|
+
and isinstance(value["path"], str) and "\0" not in value["path"] and Path(value["path"]).is_absolute()
|
|
126
|
+
and isinstance(value["sha256"], str) and re.fullmatch(r"[a-f0-9]{64}", value["sha256"]), "malformed resource binding")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def recovery_argv(binding, argv):
|
|
130
|
+
return [sys.executable, str(Path(binding["release"]["root"]) / "config/skills/devlyn-ideate/scripts/queue.py"), *argv]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_binding(binding, invoking):
|
|
134
|
+
if binding is None:
|
|
135
|
+
return
|
|
136
|
+
require(invoking is not None and binding["release"] == invoking["release"], "queue-release-mismatch")
|
|
137
|
+
require(binding == invoking, "queue defaults changed; restore the bound release")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def packet_schema(packet):
|
|
141
|
+
require(isinstance(packet, dict), "task packet must be an object")
|
|
142
|
+
version = packet.get("schema_version", 1)
|
|
143
|
+
require(type(version) is int and version in (1, 2), f"incompatible task packet version: {version!r}")
|
|
144
|
+
if version == 1:
|
|
145
|
+
return
|
|
146
|
+
require("harness" in packet and isinstance(packet.get("methodology"), dict), "malformed packet v2 harness/methodology")
|
|
147
|
+
methodology = packet["methodology"]
|
|
148
|
+
require(set(methodology) == {"defaults", "projectInstructions"} and isinstance(methodology["projectInstructions"], list),
|
|
149
|
+
"malformed packet v2 methodology")
|
|
150
|
+
for entry in methodology["projectInstructions"]:
|
|
151
|
+
resource(entry)
|
|
152
|
+
if packet["harness"] is None:
|
|
153
|
+
defaults = methodology["defaults"]
|
|
154
|
+
require(packet.get("workerPrompt") is None and (defaults is None or (isinstance(defaults, dict) and defaults.get("source") == "skill")),
|
|
155
|
+
"legacy packet has release resources")
|
|
156
|
+
if defaults is not None:
|
|
157
|
+
resource({k: v for k, v in defaults.items() if k != "source"})
|
|
158
|
+
return
|
|
159
|
+
release_ref(packet["harness"])
|
|
160
|
+
defaults = methodology["defaults"]
|
|
161
|
+
require(isinstance(defaults, dict) and defaults.get("source") == "release", "packet has no verified release defaults source")
|
|
162
|
+
resource({k: v for k, v in defaults.items() if k != "source"})
|
|
163
|
+
resource(packet.get("workerPrompt"))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def verify_packet(packet, path, skills):
|
|
167
|
+
packet_schema(packet)
|
|
168
|
+
if packet.get("schema_version", 1) == 1:
|
|
169
|
+
return # Preserve v1 execution without upgrading its resources.
|
|
170
|
+
receipt = packet.get("receipt")
|
|
171
|
+
require(isinstance(receipt, str) and "\0" not in receipt and Path(receipt).is_absolute()
|
|
172
|
+
and Path(receipt).name == "receipt.json" and Path(receipt).parent.parent.name == "devlyn-completion"
|
|
173
|
+
and Path(receipt) == Path(path).with_name("receipt.json"),
|
|
174
|
+
"malformed packet receipt path")
|
|
175
|
+
binding = read_binding(Path(receipt).parent.parent.parent)
|
|
176
|
+
if packet["harness"] is None:
|
|
177
|
+
require(binding is None, "bound task packet has no harness/defaults source")
|
|
178
|
+
# An unbound installation's own defaults change with its normal updates: never gate acceptance or delivery on
|
|
179
|
+
# their bytes (queue.py checks only their presence, before a spawn).
|
|
180
|
+
return
|
|
181
|
+
require(binding is not None, "packet queue binding is missing; restore harness.json")
|
|
182
|
+
invoking = resolve(skills)
|
|
183
|
+
ref = packet["harness"]
|
|
184
|
+
require(invoking is not None and invoking["release"] == ref,
|
|
185
|
+
f"packet-release-mismatch; use {Path(ref['root']) / 'config/skills/devlyn-ideate/scripts/queue.py'}")
|
|
186
|
+
check_binding(binding, invoking)
|
|
187
|
+
defaults = {"source": "release", **binding["defaults"]}
|
|
188
|
+
require(packet["methodology"]["defaults"] == defaults, "packet defaults differ from queue binding")
|
|
189
|
+
expected_runner = [str(Path(ref["root"]) / "config/skills/devlyn-ideate/scripts/acceptance.py"),
|
|
190
|
+
"run", "--packet", str(path)]
|
|
191
|
+
require(isinstance(packet.get("runner"), list) and packet["runner"][1:] == expected_runner, "packet runner differs from bound release")
|
|
192
|
+
prompt = packet["workerPrompt"]
|
|
193
|
+
require(prompt["path"] == str(Path(path).parent / "worker-prompt.md") and file_identity(prompt["path"]) == prompt,
|
|
194
|
+
"worker prompt changed or missing; restore the bound resource")
|
|
195
|
+
for entry in packet["methodology"]["projectInstructions"]:
|
|
196
|
+
name = Path(entry["path"]).name
|
|
197
|
+
require(name in ("CLAUDE.md", "AGENTS.md") and entry["path"] == str(Path(packet["worktree"]) / name),
|
|
198
|
+
"project instruction path escapes packet worktree")
|
|
199
|
+
committed = subprocess.run(["git", "-C", packet["worktree"], "cat-file", "blob", f"{packet['inputs_sha']}:{name}"], capture_output=True)
|
|
200
|
+
require(committed.returncode == 0 and hashlib.sha256(committed.stdout).hexdigest() == entry["sha256"],
|
|
201
|
+
f"project instruction differs from inputs commit: {name}")
|