instar 1.3.832 → 1.3.833
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/hooks/duplicate-build-start-gate.js +229 -0
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +98 -1
- package/scripts/lib/duplicate-build-check.mjs +1472 -0
- package/scripts/pre-push-gate.js +40 -0
- package/skills/instar-dev/scripts/write-trace.mjs +36 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.833.md +22 -0
- package/upgrades/side-effects/duplicate-build-guard.md +77 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* duplicate-build-start-gate.js — the duplicate-build guard's build-START
|
|
4
|
+
* structural teeth (docs/specs/duplicate-build-guard.md §3.4, FD3).
|
|
5
|
+
*
|
|
6
|
+
* A repo-local dev-lifecycle PreToolUse hook (NOT a fleet-shipped template —
|
|
7
|
+
* Migration Parity does not apply, spec §5) wired in this repo's own
|
|
8
|
+
* .claude/settings.json on Write|Edit|MultiEdit. It fires on the FIRST
|
|
9
|
+
* mutating write to any tracked repo path EXCEPT the trace/log/state paths
|
|
10
|
+
* themselves, and BLOCKS that first write (exit 2) when the recorded
|
|
11
|
+
* duplicate-build verdict is `likely-duplicate`/`verify` and no disposition
|
|
12
|
+
* has been recorded — gating the first *implementation tool call*, not
|
|
13
|
+
* turn-exit, so first-turn implementation can't slip past a Stop-event hook.
|
|
14
|
+
*
|
|
15
|
+
* Speed contract: the hook must NOT run the full scan on every tool call.
|
|
16
|
+
* Run-once semantics via a worktree-local marker
|
|
17
|
+
* (.instar/dup-build-gate.marker.json): once a build's verdict has been
|
|
18
|
+
* evaluated-and-allowed, every later call is a single existsSync + exit 0.
|
|
19
|
+
* The full check runs AT MOST once per worktree (and only when the instar-dev
|
|
20
|
+
* build-start step didn't already run it and write the stub).
|
|
21
|
+
*
|
|
22
|
+
* FAIL-OPEN (§3.4/FD5): a hook crash, an unresolvable spec, or a hard check
|
|
23
|
+
* error NEVER blocks — on a hard check error the hook writes the
|
|
24
|
+
* `check-errored` auto-stub ({verdict:"check-errored", cause:"check-error",
|
|
25
|
+
* decision:"proceed", reason:"auto: check errored (fail-open)"}) so the build
|
|
26
|
+
* proceeds AND the precommit presence-backstop still finds the field.
|
|
27
|
+
*
|
|
28
|
+
* Disposition schema (§3.4 — so the gate is not a checkbox):
|
|
29
|
+
* { verdict, cause, decision: "proceed"|"abandon", reason, acknowledgedEvidenceIds[] }
|
|
30
|
+
* A likely-duplicate proceed REQUIRES a non-empty reason AND ≥1
|
|
31
|
+
* acknowledgedEvidenceId naming a concrete evidence entry.
|
|
32
|
+
*
|
|
33
|
+
* Off-switch: INSTAR_DUP_BUILD_CHECK=off (mirrors INSTAR_PRE_PUSH_SKIP).
|
|
34
|
+
*
|
|
35
|
+
* Exit codes (Claude Code PreToolUse contract):
|
|
36
|
+
* 0 — allow the tool call
|
|
37
|
+
* 2 — BLOCK the tool call (stderr is shown to the model)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import fs from 'node:fs';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
// The hook lives at <repo>/.claude/hooks/ — the repo root is two dirs up.
|
|
46
|
+
const HOOK_ROOT = path.resolve(path.dirname(__filename), '..', '..');
|
|
47
|
+
|
|
48
|
+
const MARKER_REL = path.join('.instar', 'dup-build-gate.marker.json');
|
|
49
|
+
const STUB_REL = path.join('.instar', 'dup-build-check.json');
|
|
50
|
+
|
|
51
|
+
// Paths whose writes never trigger the gate: the guard's own state, traces,
|
|
52
|
+
// logs, scratch — plus anything outside the repo. (Spec §3.4: "EXCEPT the
|
|
53
|
+
// trace/log/state paths themselves".)
|
|
54
|
+
const EXCLUDED_PREFIXES = [
|
|
55
|
+
'.instar/', 'logs/', 'node_modules/', '.git/', 'scratchpad/', 'dist/',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function readStdin() {
|
|
59
|
+
try {
|
|
60
|
+
return fs.readFileSync(0, 'utf8');
|
|
61
|
+
} catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isOff(env) {
|
|
67
|
+
const v = String(env.INSTAR_DUP_BUILD_CHECK ?? '').toLowerCase();
|
|
68
|
+
return v === 'off' || v === '0' || v === 'false';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readJson(p) {
|
|
72
|
+
try {
|
|
73
|
+
const o = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
74
|
+
return o && typeof o === 'object' ? o : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeMarker(root, info) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.join(root, '.instar'), { recursive: true });
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(root, MARKER_REL),
|
|
85
|
+
JSON.stringify({ allowedAt: new Date().toISOString(), ...info }, null, 2) + '\n',
|
|
86
|
+
);
|
|
87
|
+
} catch { /* marker is best-effort — worst case the stub is re-read next call */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function block(lines) {
|
|
91
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main() {
|
|
96
|
+
const env = process.env;
|
|
97
|
+
if (isOff(env)) process.exit(0);
|
|
98
|
+
|
|
99
|
+
const root = env.CLAUDE_PROJECT_DIR ? path.resolve(env.CLAUDE_PROJECT_DIR) : HOOK_ROOT;
|
|
100
|
+
|
|
101
|
+
// ── HOT PATH: run-once marker → single existsSync + exit ──────────────────
|
|
102
|
+
if (fs.existsSync(path.join(root, MARKER_REL))) process.exit(0);
|
|
103
|
+
|
|
104
|
+
// Only the instar repo is in scope (the hook file ships in-tree, but a
|
|
105
|
+
// stray CLAUDE_PROJECT_DIR must not arm the gate elsewhere).
|
|
106
|
+
const pkg = readJson(path.join(root, 'package.json'));
|
|
107
|
+
if (!pkg || pkg.name !== 'instar') process.exit(0);
|
|
108
|
+
|
|
109
|
+
// ── Which file is being written? ───────────────────────────────────────────
|
|
110
|
+
let input = null;
|
|
111
|
+
try {
|
|
112
|
+
input = JSON.parse(readStdin());
|
|
113
|
+
} catch {
|
|
114
|
+
process.exit(0); // unreadable hook input → fail-open
|
|
115
|
+
}
|
|
116
|
+
const filePath = input && input.tool_input && typeof input.tool_input.file_path === 'string'
|
|
117
|
+
? input.tool_input.file_path
|
|
118
|
+
: null;
|
|
119
|
+
if (!filePath) process.exit(0);
|
|
120
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.resolve(root, filePath);
|
|
121
|
+
const rel = path.relative(root, abs);
|
|
122
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) process.exit(0); // outside the repo
|
|
123
|
+
const relNorm = rel.split(path.sep).join('/');
|
|
124
|
+
if (EXCLUDED_PREFIXES.some((p) => relNorm.startsWith(p))) process.exit(0);
|
|
125
|
+
|
|
126
|
+
// ── Read (or produce, once) the recorded verdict ───────────────────────────
|
|
127
|
+
let lib = null;
|
|
128
|
+
try {
|
|
129
|
+
lib = await import(path.join(HOOK_ROOT, 'scripts', 'lib', 'duplicate-build-check.mjs'));
|
|
130
|
+
} catch {
|
|
131
|
+
// Library unimportable = hard check error → auto-stub + allow (§3.4).
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(path.join(root, '.instar'), { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(root, STUB_REL), JSON.stringify({
|
|
135
|
+
verdict: 'check-errored', cause: 'check-error', causes: ['check-error'],
|
|
136
|
+
checkedAt: new Date().toISOString(),
|
|
137
|
+
disposition: { decision: 'proceed', reason: 'auto: check errored (fail-open)', acknowledgedEvidenceIds: [], auto: true },
|
|
138
|
+
}, null, 2) + '\n');
|
|
139
|
+
} catch { /* ignore */ }
|
|
140
|
+
writeMarker(root, { via: 'lib-unimportable' });
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let stub = lib.readStub(root);
|
|
145
|
+
if (!stub || typeof stub.verdict !== 'string') {
|
|
146
|
+
// No recorded verdict yet — the build-start step was skipped. Run the check
|
|
147
|
+
// ONCE here (Structure > Willpower: the guard doesn't depend on the skill
|
|
148
|
+
// prose having been followed). Only spec-driven builds are in scope: if no
|
|
149
|
+
// spec is resolvable on this branch, this session isn't an instar-dev build
|
|
150
|
+
// → allow + marker (the precommit spec-tag chain governs it anyway).
|
|
151
|
+
const specPath = lib.resolveSpecForAdvisory(root);
|
|
152
|
+
if (!specPath) {
|
|
153
|
+
writeMarker(root, { via: 'no-spec-resolvable' });
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
const record = lib.runDuplicateBuildCheck({ specPath, root, phase: 'build-start', env });
|
|
157
|
+
stub = { ...record };
|
|
158
|
+
if (record.verdict === 'check-errored') {
|
|
159
|
+
stub.disposition = lib.checkErroredAutoStub().disposition;
|
|
160
|
+
} else if (record.verdict === 'clear' || record.verdict === 'skipped') {
|
|
161
|
+
stub.disposition = {
|
|
162
|
+
decision: 'proceed', reason: `auto: verdict ${record.verdict}`,
|
|
163
|
+
acknowledgedEvidenceIds: [], recordedAt: new Date().toISOString(), auto: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
lib.writeStub(root, stub);
|
|
168
|
+
} catch { /* stub write best-effort; evaluation below still runs */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const v = stub.verdict;
|
|
172
|
+
if (v === 'clear' || v === 'skipped' || v === 'check-errored') {
|
|
173
|
+
writeMarker(root, { via: `verdict-${v}` });
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (v === 'likely-duplicate' || v === 'verify') {
|
|
178
|
+
const d = stub.disposition;
|
|
179
|
+
const recordCmd =
|
|
180
|
+
`node scripts/lib/duplicate-build-check.mjs --record-disposition --decision proceed ` +
|
|
181
|
+
`--reason "<why this is not a duplicate>"` +
|
|
182
|
+
(v === 'likely-duplicate' ? ` --ack <EV-id[,EV-id]>` : '');
|
|
183
|
+
if (!d || (d.decision !== 'proceed' && d.decision !== 'abandon')) {
|
|
184
|
+
const ev = (stub.evidence ?? []).slice(0, 5).map((e) => ` ${e.id} [${e.source}] ${e.detail}`);
|
|
185
|
+
block([
|
|
186
|
+
`duplicate-build gate: verdict is "${v}"${stub.cause ? ` (cause: ${stub.cause})` : ''} and no disposition is recorded — implementation writes are blocked until you decide proceed/abandon (docs/specs/duplicate-build-guard.md §3.4).`,
|
|
187
|
+
...(ev.length ? ['Evidence:', ...ev] : []),
|
|
188
|
+
'Review the overlap, then record YOUR decision (you are the authority — the tool only records it):',
|
|
189
|
+
` ${recordCmd}`,
|
|
190
|
+
'Or abandon this build if it IS a duplicate:',
|
|
191
|
+
' node scripts/lib/duplicate-build-check.mjs --record-disposition --decision abandon --reason "<duplicate of …>"',
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
if (d.decision === 'abandon') {
|
|
195
|
+
block([
|
|
196
|
+
`duplicate-build gate: this build's recorded disposition is ABANDON (${d.reason ? `reason: ${String(d.reason).slice(0, 200)}` : 'no reason recorded'}).`,
|
|
197
|
+
'Implementation writes stay blocked. If you decided to proceed after all, re-record:',
|
|
198
|
+
` ${recordCmd}`,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
if (v === 'likely-duplicate' && d.decision === 'proceed') {
|
|
202
|
+
const reasonOk = typeof d.reason === 'string' && d.reason.trim().length > 0;
|
|
203
|
+
const acks = Array.isArray(d.acknowledgedEvidenceIds)
|
|
204
|
+
? d.acknowledgedEvidenceIds.map((s) => String(s).trim()).filter(Boolean)
|
|
205
|
+
: [];
|
|
206
|
+
const evidenceIds = new Set((stub.evidence ?? []).map((e) => e.id));
|
|
207
|
+
const ackOk = acks.length >= 1 && (evidenceIds.size === 0 || acks.some((a) => evidenceIds.has(a)));
|
|
208
|
+
if (!reasonOk || !ackOk) {
|
|
209
|
+
block([
|
|
210
|
+
'duplicate-build gate: a likely-duplicate PROCEED requires a non-empty reason AND at least one acknowledgedEvidenceId naming the concrete overlap you judged non-duplicative (§3.4).',
|
|
211
|
+
`Recorded: reason=${reasonOk ? 'ok' : 'MISSING'}, acknowledgedEvidenceIds=${acks.length ? acks.join(',') : 'MISSING'}`,
|
|
212
|
+
'Re-record with:',
|
|
213
|
+
` ${recordCmd}`,
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
writeMarker(root, { via: `dispositioned-${v}-${d.decision}` });
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Unknown verdict value → fail-open.
|
|
222
|
+
writeMarker(root, { via: 'unknown-verdict' });
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main().catch(() => {
|
|
227
|
+
// A hook crash NEVER blocks (§3.4).
|
|
228
|
+
process.exit(0);
|
|
229
|
+
});
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import crypto from 'node:crypto';
|
|
29
|
-
import { execSync } from 'node:child_process';
|
|
29
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { checkEli16Overview, MIN_ELI16_CHARS } from './eli16-overview-check.mjs';
|
|
32
32
|
import { verifyProposalDerivedRunbooks } from '../skills/instar-dev/scripts/verify-proposal-derived-runbook.mjs';
|
|
@@ -963,12 +963,14 @@ if (!promotionGateResult.ok) {
|
|
|
963
963
|
assertFrameworkGenerality(inScopeFiles, validTrace.trace);
|
|
964
964
|
assertOperatorSurfaceQuality(staged, validTrace.trace);
|
|
965
965
|
assertSelfActionDeclared(addedDiffText, inScopeFiles, validTrace.trace);
|
|
966
|
+
enforceDuplicateBuildBackstop(validTrace.trace);
|
|
966
967
|
|
|
967
968
|
console.error(
|
|
968
969
|
`[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` +
|
|
969
970
|
`${REQUIRE_CONVERGENCE_REPORT ? ` + report-backed (${convergenceReportRel})` : ''}` +
|
|
970
971
|
` [cross-model: ${crossModelReview}], ELI16 overview ${eli16Rel} present (${eli16Result.charCount} chars), promotion-gate: ${promotionGateResult.reason}.`,
|
|
971
972
|
);
|
|
973
|
+
removeDupBuildMarkerOnCommitSuccess();
|
|
972
974
|
process.exit(0);
|
|
973
975
|
|
|
974
976
|
// Framework-generality review gate. Changes to the session launch/inject
|
|
@@ -1196,6 +1198,99 @@ function assertSelfActionDeclared(addedDiffText, inScopeFilesArg, trace) {
|
|
|
1196
1198
|
}
|
|
1197
1199
|
}
|
|
1198
1200
|
|
|
1201
|
+
// Duplicate-build guard — precommit PRESENCE backstop
|
|
1202
|
+
// (docs/specs/duplicate-build-guard.md §3.4, second enforced moment).
|
|
1203
|
+
// PRESENCE-ONLY: it gates on the trace FIELD existing, never on the verdict's
|
|
1204
|
+
// VALUE — it MUST accept decision:"proceed" on a likely-duplicate (the human
|
|
1205
|
+
// is the authority; a value-gate would make this validator a meaning-authority,
|
|
1206
|
+
// forbidden by docs/signal-vs-authority.md; the "hard-invariant validation"
|
|
1207
|
+
// carve-out is exactly what a structural field-validator is). It DOES
|
|
1208
|
+
// distinguish the §3.4 `check-errored` auto-stub from an author disposition:
|
|
1209
|
+
// the stub satisfies presence (never wedges the commit) but WARNS loudly.
|
|
1210
|
+
//
|
|
1211
|
+
// Rollout scoping (spec §5 + first-ship compatibility): the backstop only
|
|
1212
|
+
// REFUSES a missing field when the guard is provably live for THIS build —
|
|
1213
|
+
// either INSTAR_DUP_BUILD_CHECK is explicitly on, or the build-start check
|
|
1214
|
+
// actually ran here (its stub exists in this worktree, so the trace SHOULD
|
|
1215
|
+
// carry the field — re-running write-trace.mjs folds it in). Traces written
|
|
1216
|
+
// before the guard existed (env unset, no stub) get a loud WARN, never a
|
|
1217
|
+
// refusal. INSTAR_DUP_BUILD_CHECK=off no-ops the whole backstop.
|
|
1218
|
+
// §3.2 terminal lifecycle: at commit success the build's in-flight ledger
|
|
1219
|
+
// marker is REMOVED (mirrors PendingInjectStore "recorded at spawn, cleared
|
|
1220
|
+
// after"). Fail-open: any error is swallowed — marker cleanup must never
|
|
1221
|
+
// block a commit; a leaked marker self-heals via liveness + compaction.
|
|
1222
|
+
function removeDupBuildMarkerOnCommitSuccess() {
|
|
1223
|
+
try {
|
|
1224
|
+
const mode = String(process.env.INSTAR_DUP_BUILD_CHECK ?? '').toLowerCase();
|
|
1225
|
+
if (mode === 'off' || mode === '0' || mode === 'false') return;
|
|
1226
|
+
if (!fs.existsSync(path.join(ROOT, '.instar', 'dup-build-check.json'))) return;
|
|
1227
|
+
execFileSync('node', [path.join(ROOT, 'scripts', 'lib', 'duplicate-build-check.mjs'), '--remove-marker'], {
|
|
1228
|
+
cwd: ROOT,
|
|
1229
|
+
timeout: 3000,
|
|
1230
|
+
stdio: 'ignore',
|
|
1231
|
+
});
|
|
1232
|
+
} catch { /* fail-open — never block the commit on cleanup */ }
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function enforceDuplicateBuildBackstop(trace) {
|
|
1236
|
+
try {
|
|
1237
|
+
const mode = String(process.env.INSTAR_DUP_BUILD_CHECK ?? '').toLowerCase();
|
|
1238
|
+
if (mode === 'off' || mode === '0' || mode === 'false') return;
|
|
1239
|
+
const explicitlyOn = mode === 'on' || mode === '1' || mode === 'true';
|
|
1240
|
+
|
|
1241
|
+
const field = trace && trace.duplicateBuildCheck;
|
|
1242
|
+
if (field && typeof field === 'object' && !Array.isArray(field)) {
|
|
1243
|
+
if (field.verdict === 'check-errored') {
|
|
1244
|
+
// Presence satisfied — but a build that ran on a FAILED check is a
|
|
1245
|
+
// visible second-look signal, not silently indistinguishable from an
|
|
1246
|
+
// author-reviewed proceed (§3.4). WARN, never block.
|
|
1247
|
+
console.error('');
|
|
1248
|
+
console.error('┌──────────────────────────────────────────────────────────────────┐');
|
|
1249
|
+
console.error('│ ⚠ DUPLICATE-BUILD CHECK ERRORED (fail-open auto-stub). │');
|
|
1250
|
+
console.error('│ This build proceeded WITHOUT a working duplicate-build check. │');
|
|
1251
|
+
console.error('│ NOT blocked — but give the overlap question a second look: │');
|
|
1252
|
+
console.error('│ node scripts/lib/duplicate-build-check.mjs <specPath> │');
|
|
1253
|
+
console.error('└──────────────────────────────────────────────────────────────────┘');
|
|
1254
|
+
console.error('');
|
|
1255
|
+
}
|
|
1256
|
+
return; // presence-only — the verdict's VALUE is never gated on.
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const stubExists = fs.existsSync(path.join(ROOT, '.instar', 'dup-build-check.json'));
|
|
1260
|
+
if (explicitlyOn || stubExists) {
|
|
1261
|
+
blockCommit(
|
|
1262
|
+
inScopeFiles,
|
|
1263
|
+
[
|
|
1264
|
+
'Duplicate-build backstop: the trace carries no duplicateBuildCheck field.',
|
|
1265
|
+
'',
|
|
1266
|
+
'The duplicate-build guard is live for this build' +
|
|
1267
|
+
(stubExists ? ' (its check stub exists at .instar/dup-build-check.json)' : ' (INSTAR_DUP_BUILD_CHECK is on)') + ',',
|
|
1268
|
+
'so the trace must record the check verdict + your proceed/abandon disposition.',
|
|
1269
|
+
'',
|
|
1270
|
+
'Fix:',
|
|
1271
|
+
' 1. Run the check (if you have not): node scripts/lib/duplicate-build-check.mjs <specPath>',
|
|
1272
|
+
' 2. On a verify/likely-duplicate verdict, record your disposition:',
|
|
1273
|
+
' node scripts/lib/duplicate-build-check.mjs --record-disposition --decision proceed --reason "…" [--ack EV-1]',
|
|
1274
|
+
' 3. Re-run write-trace.mjs (it folds the stub into the trace) and commit fresh.',
|
|
1275
|
+
'',
|
|
1276
|
+
'This backstop is PRESENCE-ONLY: it never gates on the verdict value —',
|
|
1277
|
+
'a recorded proceed-on-likely-duplicate passes (you are the authority).',
|
|
1278
|
+
'(docs/specs/duplicate-build-guard.md §3.4; off-switch: INSTAR_DUP_BUILD_CHECK=off)',
|
|
1279
|
+
].join('\n'),
|
|
1280
|
+
);
|
|
1281
|
+
} else {
|
|
1282
|
+
// Guard not yet live for this build — advisory only (a trace written
|
|
1283
|
+
// before the guard existed must not be refused retroactively).
|
|
1284
|
+
console.error(
|
|
1285
|
+
'[instar-dev-precommit] note: trace has no duplicateBuildCheck field (duplicate-build guard not live for this build — advisory only).',
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
} catch {
|
|
1289
|
+
// Fail-open (FD5): the backstop must never crash the gate on its own bug.
|
|
1290
|
+
// (blockCommit exits the process directly, so a real refusal never lands here.)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1199
1294
|
function blockCommit(files, reason) {
|
|
1200
1295
|
console.error('');
|
|
1201
1296
|
console.error('╔════════════════════════════════════════════════════════════════════╗');
|
|
@@ -1399,11 +1494,13 @@ function enforceTier1(trace, traceFile) {
|
|
|
1399
1494
|
assertFrameworkGenerality(inScopeFiles, trace);
|
|
1400
1495
|
assertOperatorSurfaceQuality(staged, trace);
|
|
1401
1496
|
assertSelfActionDeclared(addedDiffText, inScopeFiles, trace);
|
|
1497
|
+
enforceDuplicateBuildBackstop(trace);
|
|
1402
1498
|
|
|
1403
1499
|
console.error(
|
|
1404
1500
|
`[instar-dev-precommit] OK (Tier 1) — trace ${traceName} covers ${inScopeFiles.length} in-scope file(s), ` +
|
|
1405
1501
|
`ELI16 ${eli16Rel} (${eli16Content.trim().length} chars) + side-effects ${sideEffectsRel} staged & verified. ` +
|
|
1406
1502
|
`No converged spec required for Tier 1.`,
|
|
1407
1503
|
);
|
|
1504
|
+
removeDupBuildMarkerOnCommitSuccess();
|
|
1408
1505
|
process.exit(0);
|
|
1409
1506
|
}
|