instar 0.28.41 → 0.28.44
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/dist/commands/review.js +8 -1
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +24 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts +34 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +30 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/core/ContextHierarchy.d.ts +9 -0
- package/dist/core/ContextHierarchy.d.ts.map +1 -1
- package/dist/core/ContextHierarchy.js +20 -5
- package/dist/core/ContextHierarchy.js.map +1 -1
- package/dist/core/MachineIdentity.d.ts +8 -0
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +15 -0
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/MessagingToneGate.d.ts +46 -0
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +104 -24
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +5 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/OutboundDedupGate.d.ts +56 -0
- package/dist/core/OutboundDedupGate.d.ts.map +1 -0
- package/dist/core/OutboundDedupGate.js +90 -0
- package/dist/core/OutboundDedupGate.js.map +1 -0
- package/dist/core/SharedStateLedger.d.ts +111 -0
- package/dist/core/SharedStateLedger.d.ts.map +1 -0
- package/dist/core/SharedStateLedger.js +174 -0
- package/dist/core/SharedStateLedger.js.map +1 -0
- package/dist/core/UpdateChecker.d.ts.map +1 -1
- package/dist/core/UpdateChecker.js +6 -2
- package/dist/core/UpdateChecker.js.map +1 -1
- package/dist/core/junk-payload.d.ts +14 -0
- package/dist/core/junk-payload.d.ts.map +1 -0
- package/dist/core/junk-payload.js +32 -0
- package/dist/core/junk-payload.js.map +1 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +13 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/monitoring/SessionRecovery.d.ts +27 -0
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -1
- package/dist/monitoring/SessionRecovery.js +61 -4
- package/dist/monitoring/SessionRecovery.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobLoader.d.ts +4 -0
- package/dist/scheduler/JobLoader.d.ts.map +1 -1
- package/dist/scheduler/JobLoader.js +7 -1
- package/dist/scheduler/JobLoader.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +207 -14
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +295 -0
- package/scripts/pre-push-gate.js +65 -0
- package/src/data/builtin-manifest.json +49 -49
- package/upgrades/0.28.26.md +21 -0
- package/upgrades/0.28.27.md +17 -0
- package/upgrades/0.28.28.md +23 -0
- package/upgrades/0.28.29.md +17 -0
- package/upgrades/0.28.42.md +25 -0
- package/upgrades/0.28.43.md +106 -0
- package/upgrades/0.28.44.md +21 -0
- package/upgrades/side-effects/0.28.43.md +57 -0
- package/upgrades/side-effects/fix-auto-ack-echo-loop.md +36 -0
- package/upgrades/side-effects/instar-dev-skill.md +137 -0
- package/upgrades/side-effects/outbound-signal-authority-rework.md +160 -0
- package/upgrades/side-effects/retrospective-drain-and-principle.md +113 -0
- package/upgrades/side-effects/skill-audience-clarification.md +54 -0
- package/upgrades/side-effects/state-file-self-heal-stage-1.md +162 -0
package/package.json
CHANGED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* instar-dev-precommit.js — pre-commit gate enforcing /instar-dev skill usage.
|
|
4
|
+
*
|
|
5
|
+
* Runs in the instar repo's .husky/pre-commit. For any commit that touches
|
|
6
|
+
* behavior (src/, scripts/, .husky/, or skills/), this gate requires:
|
|
7
|
+
*
|
|
8
|
+
* 1. A fresh trace file exists in .instar/instar-dev-traces/ (< 60 min old).
|
|
9
|
+
* 2. The trace's coveredFiles is a superset of the staged files in scope.
|
|
10
|
+
* 3. The trace references an artifact file that exists.
|
|
11
|
+
* 4. The artifact's content sha256 matches what the trace recorded.
|
|
12
|
+
* 5. The artifact is longer than a stub (> 200 chars of real content).
|
|
13
|
+
*
|
|
14
|
+
* If the commit touches nothing in scope (pure docs, release notes,
|
|
15
|
+
* gitignore tweaks, etc.), the gate passes through.
|
|
16
|
+
*
|
|
17
|
+
* Bypass is structurally discouraged. The standard `--no-verify` still
|
|
18
|
+
* works (git itself owns that flag), but any such commit is visible in
|
|
19
|
+
* git history and flagged by the post-push release analyzer.
|
|
20
|
+
*
|
|
21
|
+
* Exit codes:
|
|
22
|
+
* 0 — pass
|
|
23
|
+
* 1 — block
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import { execSync } from 'node:child_process';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = path.dirname(__filename);
|
|
34
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
35
|
+
const TRACES_DIR = path.join(ROOT, '.instar', 'instar-dev-traces');
|
|
36
|
+
const WINDOW_MS = 60 * 60 * 1000; // 60 minutes
|
|
37
|
+
const MIN_ARTIFACT_CHARS = 200;
|
|
38
|
+
|
|
39
|
+
// ─── Step 0: skip gate for merge commits ─────────────────────────────────
|
|
40
|
+
// Merge commits integrate already-reviewed code from another branch/machine.
|
|
41
|
+
// The side-effects review was done when those commits were originally authored.
|
|
42
|
+
if (fs.existsSync(path.join(ROOT, '.git', 'MERGE_HEAD'))) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Step 1: inspect staged files ────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
let stagedOutput;
|
|
49
|
+
try {
|
|
50
|
+
stagedOutput = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
51
|
+
cwd: ROOT,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// git not available or not in a git repo — can't verify; fail-open.
|
|
56
|
+
console.error('[instar-dev-precommit] git not available — skipping gate');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const staged = stagedOutput
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
|
|
65
|
+
if (staged.length === 0) {
|
|
66
|
+
// No staged files — nothing to check.
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Step 2: classify staged files ───────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function inScope(file) {
|
|
73
|
+
// Files that require review: anything that ships behavior.
|
|
74
|
+
if (file.startsWith('src/')) return true;
|
|
75
|
+
if (file.startsWith('scripts/')) return true;
|
|
76
|
+
if (file.startsWith('.husky/')) return true;
|
|
77
|
+
if (file.startsWith('skills/') && file.endsWith('SKILL.md')) return true;
|
|
78
|
+
if (file.startsWith('skills/') && (file.endsWith('.sh') || file.endsWith('.mjs') || file.endsWith('.js'))) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inScopeFiles = staged.filter(inScope);
|
|
83
|
+
|
|
84
|
+
if (inScopeFiles.length === 0) {
|
|
85
|
+
// Pure docs / release notes / config — pass through.
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Step 3: bootstrap exception ─────────────────────────────────────────
|
|
90
|
+
// If this commit itself is introducing the gate (i.e., scripts/instar-dev-precommit.js
|
|
91
|
+
// is staged as "A"), the gate isn't "in effect" yet — its own first-ship can't
|
|
92
|
+
// reference itself. We detect this and pass, documenting the bypass.
|
|
93
|
+
|
|
94
|
+
let addedOutput = '';
|
|
95
|
+
try {
|
|
96
|
+
addedOutput = execSync('git diff --cached --name-only --diff-filter=A', {
|
|
97
|
+
cwd: ROOT,
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
const addedFiles = addedOutput.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
const BOOTSTRAP_TRIGGERS = [
|
|
105
|
+
'scripts/instar-dev-precommit.js',
|
|
106
|
+
'skills/spec-converge/SKILL.md',
|
|
107
|
+
];
|
|
108
|
+
const bootstrapTrigger = addedFiles.find((f) => BOOTSTRAP_TRIGGERS.includes(f));
|
|
109
|
+
if (bootstrapTrigger) {
|
|
110
|
+
console.error(
|
|
111
|
+
`[instar-dev-precommit] bootstrap commit detected (${bootstrapTrigger} is being added) — passing. All future commits will be gated by the full spec-tag chain.`,
|
|
112
|
+
);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Step 4: find a fresh trace ──────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(TRACES_DIR)) {
|
|
119
|
+
blockCommit(inScopeFiles, 'No trace directory found. Run the /instar-dev skill to produce a trace before committing.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const traceEntries = fs
|
|
123
|
+
.readdirSync(TRACES_DIR)
|
|
124
|
+
.filter((f) => f.endsWith('.json'))
|
|
125
|
+
.map((f) => ({
|
|
126
|
+
file: path.join(TRACES_DIR, f),
|
|
127
|
+
mtime: fs.statSync(path.join(TRACES_DIR, f)).mtimeMs,
|
|
128
|
+
}))
|
|
129
|
+
.filter((e) => Date.now() - e.mtime < WINDOW_MS)
|
|
130
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
131
|
+
|
|
132
|
+
if (traceEntries.length === 0) {
|
|
133
|
+
blockCommit(
|
|
134
|
+
inScopeFiles,
|
|
135
|
+
'No fresh trace found (< 60 min old) in .instar/instar-dev-traces/. Run the /instar-dev skill to produce one.',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Step 5: validate most recent trace against staged files ─────────────
|
|
140
|
+
|
|
141
|
+
let validTrace = null;
|
|
142
|
+
const attempts = [];
|
|
143
|
+
|
|
144
|
+
for (const entry of traceEntries) {
|
|
145
|
+
let trace;
|
|
146
|
+
try {
|
|
147
|
+
trace = JSON.parse(fs.readFileSync(entry.file, 'utf8'));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
attempts.push(`${path.basename(entry.file)}: malformed JSON`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (trace.phase !== 'complete') {
|
|
154
|
+
attempts.push(`${path.basename(entry.file)}: trace phase is "${trace.phase}", expected "complete"`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const covered = new Set(trace.coveredFiles || []);
|
|
159
|
+
const missing = inScopeFiles.filter((f) => !covered.has(f));
|
|
160
|
+
if (missing.length > 0) {
|
|
161
|
+
attempts.push(
|
|
162
|
+
`${path.basename(entry.file)}: trace's coveredFiles does not include: ${missing.join(', ')}`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const artifactPath = path.resolve(ROOT, trace.artifactPath);
|
|
168
|
+
if (!fs.existsSync(artifactPath)) {
|
|
169
|
+
attempts.push(`${path.basename(entry.file)}: artifact ${trace.artifactPath} does not exist`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const artifactContent = fs.readFileSync(artifactPath, 'utf8');
|
|
174
|
+
if (artifactContent.trim().length < MIN_ARTIFACT_CHARS) {
|
|
175
|
+
attempts.push(
|
|
176
|
+
`${path.basename(entry.file)}: artifact is too short (${artifactContent.trim().length} chars, need ${MIN_ARTIFACT_CHARS})`,
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sha = crypto.createHash('sha256').update(artifactContent).digest('hex');
|
|
182
|
+
if (trace.artifactSha256 && trace.artifactSha256 !== sha) {
|
|
183
|
+
attempts.push(
|
|
184
|
+
`${path.basename(entry.file)}: artifact content has changed since the trace was written (sha mismatch)`,
|
|
185
|
+
);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Also verify the artifact is staged — it must ship alongside the code.
|
|
190
|
+
if (!staged.includes(trace.artifactPath)) {
|
|
191
|
+
attempts.push(
|
|
192
|
+
`${path.basename(entry.file)}: artifact ${trace.artifactPath} is not staged for commit — it must ship alongside the change`,
|
|
193
|
+
);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
validTrace = { entry, trace };
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!validTrace) {
|
|
202
|
+
blockCommit(
|
|
203
|
+
inScopeFiles,
|
|
204
|
+
[
|
|
205
|
+
'No valid trace matched the staged changes. Attempts:',
|
|
206
|
+
...attempts.map((a) => ` • ${a}`),
|
|
207
|
+
'',
|
|
208
|
+
'Run the /instar-dev skill, produce the side-effects artifact, stage the artifact, and write a fresh trace before committing.',
|
|
209
|
+
].join('\n'),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Step 6: spec-tag verification ───────────────────────────────────────
|
|
214
|
+
// Every in-scope change must reference a spec that has (a) been through
|
|
215
|
+
// /spec-converge to convergence, and (b) been explicitly approved by the
|
|
216
|
+
// user. The trace's `specPath` field points at the spec file. We parse its
|
|
217
|
+
// YAML frontmatter and verify both tags are present.
|
|
218
|
+
|
|
219
|
+
const spec = validTrace.trace.specPath;
|
|
220
|
+
if (!spec) {
|
|
221
|
+
blockCommit(
|
|
222
|
+
inScopeFiles,
|
|
223
|
+
[
|
|
224
|
+
'Trace does not reference a spec (trace.specPath is missing).',
|
|
225
|
+
'',
|
|
226
|
+
'Every in-scope change must be driven by a spec that has passed /spec-converge',
|
|
227
|
+
'and been approved by the user. Write the trace with --spec <path>.',
|
|
228
|
+
].join('\n'),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const specPath = path.resolve(ROOT, spec);
|
|
233
|
+
if (!fs.existsSync(specPath)) {
|
|
234
|
+
blockCommit(
|
|
235
|
+
inScopeFiles,
|
|
236
|
+
`Spec file ${spec} (referenced by trace) does not exist.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
241
|
+
const specFmMatch = specContent.match(/^---\n([\s\S]*?)\n---\n/);
|
|
242
|
+
if (!specFmMatch) {
|
|
243
|
+
blockCommit(
|
|
244
|
+
inScopeFiles,
|
|
245
|
+
`Spec ${spec} has no YAML frontmatter. It cannot carry the required review-convergence and approved tags.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const specFm = specFmMatch[1];
|
|
249
|
+
const convergenceMatch = specFm.match(/^\s*review-convergence\s*:\s*["']?([^"'\n]+)/m);
|
|
250
|
+
const approvedMatch = specFm.match(/^\s*approved\s*:\s*(true|"true"|'true')/m);
|
|
251
|
+
|
|
252
|
+
if (!convergenceMatch) {
|
|
253
|
+
blockCommit(
|
|
254
|
+
inScopeFiles,
|
|
255
|
+
[
|
|
256
|
+
`Spec ${spec} is not tagged review-convergence.`,
|
|
257
|
+
'Run /spec-converge on this spec before committing the change.',
|
|
258
|
+
].join('\n'),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!approvedMatch) {
|
|
263
|
+
blockCommit(
|
|
264
|
+
inScopeFiles,
|
|
265
|
+
[
|
|
266
|
+
`Spec ${spec} has review-convergence but no approved: true tag.`,
|
|
267
|
+
'The user must review the convergence report and apply the approved tag',
|
|
268
|
+
'before /instar-dev can ship this change.',
|
|
269
|
+
].join('\n'),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Pass ────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
console.error(
|
|
276
|
+
`[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved.`,
|
|
277
|
+
);
|
|
278
|
+
process.exit(0);
|
|
279
|
+
|
|
280
|
+
function blockCommit(files, reason) {
|
|
281
|
+
console.error('');
|
|
282
|
+
console.error('╔════════════════════════════════════════════════════════════════════╗');
|
|
283
|
+
console.error('║ /instar-dev gate — commit BLOCKED ║');
|
|
284
|
+
console.error('╚════════════════════════════════════════════════════════════════════╝');
|
|
285
|
+
console.error('');
|
|
286
|
+
console.error('In-scope staged files requiring side-effects review:');
|
|
287
|
+
for (const f of files) console.error(` • ${f}`);
|
|
288
|
+
console.error('');
|
|
289
|
+
console.error('Reason:');
|
|
290
|
+
reason.split('\n').forEach((line) => console.error(` ${line}`));
|
|
291
|
+
console.error('');
|
|
292
|
+
console.error('See docs/signal-vs-authority.md and skills/instar-dev/SKILL.md for details.');
|
|
293
|
+
console.error('');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
package/scripts/pre-push-gate.js
CHANGED
|
@@ -203,6 +203,71 @@ try {
|
|
|
203
203
|
// Git commands may fail in CI or detached HEAD — skip gracefully
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// ── 5. Side-effects review artifact ───────────────────────────────────
|
|
207
|
+
// If the upgrade notes claim a fix or feature (anything that would require
|
|
208
|
+
// an Evidence section), a matching side-effects review artifact must exist
|
|
209
|
+
// in upgrades/side-effects/. This enforces the /instar-dev process at push
|
|
210
|
+
// time — the pre-commit hook catches it earlier per-commit, this is the
|
|
211
|
+
// release-level re-check.
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
const FIX_PATTERNS = [
|
|
215
|
+
/\bfix(es|ed|ing)?\b/i,
|
|
216
|
+
/\bbug(fix)?\b/i,
|
|
217
|
+
/\bregression\b/i,
|
|
218
|
+
/\bresolves?\b/i,
|
|
219
|
+
/\bresolved\b/i,
|
|
220
|
+
/\bcrashes?\b/i,
|
|
221
|
+
/\bcrashed\b/i,
|
|
222
|
+
/\bcrashing\b/i,
|
|
223
|
+
/\bbroken\b/i,
|
|
224
|
+
/\bstall(s|ed|ing)?\b/i,
|
|
225
|
+
/\bfeature\b/i,
|
|
226
|
+
/\badd(s|ed|ing)?\b/i,
|
|
227
|
+
/\bnew\b/i,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const guidePath = versionedGuideExists ? versionedGuidePath : nextPath;
|
|
231
|
+
if (fs.existsSync(guidePath)) {
|
|
232
|
+
const guideContent = fs.readFileSync(guidePath, 'utf-8');
|
|
233
|
+
// Extract "## What Changed" section
|
|
234
|
+
const whatChangedMatch = guideContent.match(/## What Changed\s*([\s\S]*?)(?=\n##\s|$)/);
|
|
235
|
+
const whatChanged = whatChangedMatch ? whatChangedMatch[1] : '';
|
|
236
|
+
|
|
237
|
+
const qualifies = FIX_PATTERNS.some((p) => p.test(whatChanged));
|
|
238
|
+
|
|
239
|
+
if (qualifies) {
|
|
240
|
+
const sideEffectsDir = path.join(ROOT, 'upgrades', 'side-effects');
|
|
241
|
+
const artifactName = versionedGuideExists ? `${version}.md` : null;
|
|
242
|
+
let artifactFound = false;
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(sideEffectsDir)) {
|
|
245
|
+
const files = fs.readdirSync(sideEffectsDir).filter((f) => f.endsWith('.md'));
|
|
246
|
+
if (artifactName) {
|
|
247
|
+
artifactFound = files.includes(artifactName);
|
|
248
|
+
} else {
|
|
249
|
+
// For NEXT.md, any fresh artifact from the last 24h counts.
|
|
250
|
+
// The expectation is that during release cut, NEXT.md -> <version>.md
|
|
251
|
+
// rename will pair with the artifact rename as well.
|
|
252
|
+
const recent = files.filter((f) => {
|
|
253
|
+
const stat = fs.statSync(path.join(sideEffectsDir, f));
|
|
254
|
+
return Date.now() - stat.mtimeMs < 24 * 60 * 60 * 1000;
|
|
255
|
+
});
|
|
256
|
+
artifactFound = recent.length > 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!artifactFound) {
|
|
261
|
+
errors.push(
|
|
262
|
+
`Upgrade notes claim a fix/feature but no matching side-effects review artifact found in upgrades/side-effects/. ` +
|
|
263
|
+
`Every change qualifying for review must ship with an artifact produced via the /instar-dev skill. ` +
|
|
264
|
+
`See skills/instar-dev/SKILL.md and docs/signal-vs-authority.md.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
206
271
|
// ── Report ────────────────────────────────────────────────────────────
|
|
207
272
|
|
|
208
273
|
if (errors.length > 0 || warnings.length > 0) {
|