instar 1.3.999 → 1.3.1001
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/package.json
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
import fs from 'node:fs';
|
|
47
47
|
import path from 'node:path';
|
|
48
48
|
import crypto from 'node:crypto';
|
|
49
|
+
import { execFileSync } from 'node:child_process';
|
|
49
50
|
|
|
50
51
|
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..');
|
|
51
52
|
|
|
@@ -157,6 +158,107 @@ function buildToolchain() {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Verify the commit gate is actually INSTALLED before writing a trace that implies it ran.
|
|
163
|
+
*
|
|
164
|
+
* THE FAILURE THIS EXISTS TO CATCH. A worktree created with `git worktree add` and a
|
|
165
|
+
* symlinked (or absent) node_modules has no husky shim, so `core.hooksPath` points at a
|
|
166
|
+
* directory that does not exist. `git commit` then runs NO hook: it prints nothing and
|
|
167
|
+
* succeeds. **A working gate and an uninstalled gate look identical on screen — silence.**
|
|
168
|
+
* That is not hypothetical; it happened on 2026-07-27 and a whole change was committed
|
|
169
|
+
* believing the gate had passed it, when the gate had never executed.
|
|
170
|
+
*
|
|
171
|
+
* The gate cannot report its own absence — an uninstalled hook cannot run to say it is
|
|
172
|
+
* uninstalled. So the check belongs at the nearest chokepoint the agent DOES invoke by
|
|
173
|
+
* hand, which is this script. Writing a trace is the step that asserts "this change came
|
|
174
|
+
* through the skill", and that assertion is false if the gate cannot execute.
|
|
175
|
+
*
|
|
176
|
+
* SCOPE HONESTY: this is a SIGNAL, not an authority. It refuses to write the trace and
|
|
177
|
+
* says why; it cannot block a commit, because the very condition it detects is the one
|
|
178
|
+
* where no commit hook runs. Without a trace an in-scope commit is refused ANYWAY once the
|
|
179
|
+
* hook IS installed — so the two layers compose rather than overlap. Its whole job is to
|
|
180
|
+
* turn a silent absence into a loud one at the moment the agent would otherwise proceed.
|
|
181
|
+
*
|
|
182
|
+
* Set INSTAR_DEV_ALLOW_UNINSTALLED_GATE=1 to proceed anyway (a genuinely hookless
|
|
183
|
+
* environment, e.g. a test harness). The override is recorded IN THE TRACE rather than
|
|
184
|
+
* merely permitted, so a trace written without a live gate can never later be mistaken for
|
|
185
|
+
* one the gate approved.
|
|
186
|
+
*/
|
|
187
|
+
function inspectGateInstallation() {
|
|
188
|
+
const result = { installed: false, reason: '', hooksPath: null };
|
|
189
|
+
let hooksPath;
|
|
190
|
+
try {
|
|
191
|
+
hooksPath = execFileSync('git', ['config', '--get', 'core.hooksPath'], {
|
|
192
|
+
cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
193
|
+
}).trim();
|
|
194
|
+
} catch {
|
|
195
|
+
hooksPath = '';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!hooksPath) {
|
|
199
|
+
// No hooksPath set at all — git falls back to .git/hooks. Check there instead of
|
|
200
|
+
// assuming absence: a repo may legitimately install the hook the classic way.
|
|
201
|
+
let gitDir;
|
|
202
|
+
try {
|
|
203
|
+
gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
|
|
204
|
+
cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
205
|
+
}).trim();
|
|
206
|
+
} catch {
|
|
207
|
+
// NOT A GIT REPOSITORY AT ALL — the check does not apply.
|
|
208
|
+
//
|
|
209
|
+
// This is not a loophole, it is the check's actual scope. The hazard being guarded
|
|
210
|
+
// is "a git repo whose commit hook will not run". Where there is no repo there is no
|
|
211
|
+
// commit, so there is nothing to gate and nothing to be misled about. Test harnesses
|
|
212
|
+
// that drive this script inside a bare temp directory land here.
|
|
213
|
+
//
|
|
214
|
+
// Deliberately NOT solved by making every such harness set the override env var:
|
|
215
|
+
// that would push the burden onto whoever writes the next test remembering to do it,
|
|
216
|
+
// which is the willpower-over-structure trade this whole change exists to remove.
|
|
217
|
+
// It is also not a meaningful bypass — an agent cannot make the instar repo stop
|
|
218
|
+
// being a git repository.
|
|
219
|
+
return { installed: true, reason: '', hooksPath: null, notApplicable: true };
|
|
220
|
+
}
|
|
221
|
+
const classic = path.resolve(ROOT, gitDir, 'hooks', 'pre-commit');
|
|
222
|
+
if (fs.existsSync(classic)) return { installed: true, reason: '', hooksPath: classic };
|
|
223
|
+
return { ...result, reason: 'core.hooksPath is unset and .git/hooks/pre-commit does not exist' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
result.hooksPath = hooksPath;
|
|
227
|
+
const resolved = path.resolve(ROOT, hooksPath);
|
|
228
|
+
if (!fs.existsSync(resolved)) {
|
|
229
|
+
return { ...result, reason: `core.hooksPath is "${hooksPath}" but that directory does not exist — husky is not installed in this worktree (run \`npm ci\` then \`npx husky\`)` };
|
|
230
|
+
}
|
|
231
|
+
const preCommit = path.join(resolved, 'pre-commit');
|
|
232
|
+
if (!fs.existsSync(preCommit)) {
|
|
233
|
+
return { ...result, reason: `core.hooksPath is "${hooksPath}" but it contains no pre-commit hook` };
|
|
234
|
+
}
|
|
235
|
+
return { installed: true, reason: '', hooksPath };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const gate = inspectGateInstallation();
|
|
239
|
+
const gateOverridden = process.env.INSTAR_DEV_ALLOW_UNINSTALLED_GATE === '1';
|
|
240
|
+
if (!gate.installed && !gateOverridden) {
|
|
241
|
+
console.error('');
|
|
242
|
+
console.error(' ╔════════════════════════════════════════════════════════════════════╗');
|
|
243
|
+
console.error(' ║ REFUSING to write a trace — the commit gate is NOT installed ║');
|
|
244
|
+
console.error(' ╚════════════════════════════════════════════════════════════════════╝');
|
|
245
|
+
console.error('');
|
|
246
|
+
console.error(` ${gate.reason}`);
|
|
247
|
+
console.error('');
|
|
248
|
+
console.error(' A trace asserts this change came through /instar-dev. With no hook');
|
|
249
|
+
console.error(' installed, `git commit` runs NOTHING and succeeds silently — a passing');
|
|
250
|
+
console.error(' gate and an absent gate look identical on screen. Writing the trace now');
|
|
251
|
+
console.error(' would record an approval that was never given.');
|
|
252
|
+
console.error('');
|
|
253
|
+
console.error(' Fix it in this worktree: npm ci && npx husky');
|
|
254
|
+
console.error(' Then verify: git config core.hooksPath && ls .husky/_');
|
|
255
|
+
console.error('');
|
|
256
|
+
console.error(' If this environment genuinely has no hooks (e.g. a test harness), set');
|
|
257
|
+
console.error(' INSTAR_DEV_ALLOW_UNINSTALLED_GATE=1 — the override is recorded in the trace.');
|
|
258
|
+
console.error('');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
160
262
|
const artifactPath = path.resolve(ROOT, artifact);
|
|
161
263
|
if (!fs.existsSync(artifactPath)) {
|
|
162
264
|
console.error(`Artifact not found: ${artifact}`);
|
|
@@ -235,6 +337,10 @@ const trace = {
|
|
|
235
337
|
// Duplicate-build guard §3.4 — omitted when no stub exists so pre-guard
|
|
236
338
|
// traces round-trip byte-identically.
|
|
237
339
|
...(duplicateBuildCheck ? { duplicateBuildCheck } : {}),
|
|
340
|
+
// Recorded ONLY when the gate-installation check was overridden, so a trace written
|
|
341
|
+
// without a live commit gate can never later be mistaken for one the gate approved.
|
|
342
|
+
// Absent on every normal trace, so existing traces round-trip byte-identically.
|
|
343
|
+
...(gate.installed ? {} : { gateInstallationOverridden: true, gateInstallationReason: gate.reason }),
|
|
238
344
|
};
|
|
239
345
|
|
|
240
346
|
fs.writeFileSync(traceFile, JSON.stringify(trace, null, 2) + '\n');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-27T10:24:33.892Z",
|
|
5
|
+
"instarVersion": "1.3.1001",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`skills/instar-dev/scripts/write-trace.mjs` now verifies the instar-dev commit gate is actually
|
|
9
|
+
installed before writing a trace, and refuses with an explanation when it is not.
|
|
10
|
+
|
|
11
|
+
A worktree created with `git worktree add` inherits `core.hooksPath` (typically `.husky/_`) from the
|
|
12
|
+
shared repo config, while that directory does not exist until `npm ci` plus `npx husky` have run. In
|
|
13
|
+
that state `git commit` executes no hook: it prints nothing and succeeds. A working gate and an
|
|
14
|
+
uninstalled gate are indistinguishable on screen, because both are silent — and on 2026-07-27 a
|
|
15
|
+
change was committed on the belief the gate had passed it when the gate had never run.
|
|
16
|
+
|
|
17
|
+
The gate cannot detect its own absence, since an uninstalled hook cannot execute to say so. The check
|
|
18
|
+
therefore lives at the nearest chokepoint invoked by hand: writing the trace, which is the step that
|
|
19
|
+
asserts the change came through the skill. `INSTAR_DEV_ALLOW_UNINSTALLED_GATE=1` overrides it, and
|
|
20
|
+
the override is recorded in the trace so an unguarded trace can never be mistaken for an approved one.
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
None — internal change (no user-facing surface).
|
|
25
|
+
|
|
26
|
+
## Summary of New Capabilities
|
|
27
|
+
|
|
28
|
+
None — internal change (no user-facing surface).
|
|
29
|
+
|
|
30
|
+
## Evidence
|
|
31
|
+
|
|
32
|
+
Verified against a real hookless worktree before any test was written — `core.hooksPath` was already
|
|
33
|
+
`.husky/_` in a freshly added worktree with no shim, confirming silently-no-gate is the default state
|
|
34
|
+
rather than an exotic misconfiguration:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
core.hooksPath is ".husky/_" but that directory does not exist — husky is not installed
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Falsified by neutering the check in the script:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
× THE FIX: refuses to write a trace when hooksPath points at a missing directory
|
|
44
|
+
→ expected +0 to be 1
|
|
45
|
+
× refuses when hooksPath is unset AND no classic hook exists
|
|
46
|
+
Tests 2 failed | 4 passed (6)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Restored byte-identical. The over-block was found by running the suites chosen from the diff rather
|
|
50
|
+
than the files authored: the first implementation broke seven tests in `write-trace-tier` and
|
|
51
|
+
`duplicate-build-guard-gates`, both of which drive the script inside bare temp directories. Scoping
|
|
52
|
+
the check to git repositories only fixed it — a directory that is not a repo has no commit to gate.
|
|
53
|
+
Final: `Test Files 3 passed (3) · Tests 23 passed (23)`.
|
|
54
|
+
|
|
55
|
+
## Known limits
|
|
56
|
+
|
|
57
|
+
The check verifies a pre-commit hook exists; it does not verify it is executable, non-empty, or that
|
|
58
|
+
it is the instar-dev gate rather than another hook. A hook that exits 0 immediately would satisfy it
|
|
59
|
+
while gating nothing. It also cannot help retroactively — a commit already made without the gate
|
|
60
|
+
leaves no marker, because nothing ran to leave one.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Adds `docs/specs/decision-replayability-standard.md` (plus its ELI16 companion) — the written
|
|
9
|
+
standard for what must be recorded so a decision can be replayed and re-evaluated later, rather than
|
|
10
|
+
merely asserted to have happened.
|
|
11
|
+
|
|
12
|
+
It answers the question the operator asked directly: **is a screenshot an acceptable thing to store?**
|
|
13
|
+
For a TERMINAL prompt the answer is no, and not for privacy reasons — the scrubbed pane TEXT of the
|
|
14
|
+
prompt region carries identical information to an image of it, but only text is scrubbable,
|
|
15
|
+
greppable, and diffable. Text is therefore *more* auditable, not less. Images remain the right medium
|
|
16
|
+
only where no text form exists, such as a GUI or a browser.
|
|
17
|
+
|
|
18
|
+
The standard carries three bounds so the rule cannot quietly become unbounded surveillance: capture
|
|
19
|
+
the prompt REGION rather than full scrollback; scrub before write, never after; and mark a record
|
|
20
|
+
`replayability: degraded` when scrubbing has hollowed it out, so a log can never silently become
|
|
21
|
+
useless while still looking complete.
|
|
22
|
+
|
|
23
|
+
## What to Tell Your User
|
|
24
|
+
|
|
25
|
+
None — internal change (no user-facing surface).
|
|
26
|
+
|
|
27
|
+
## Summary of New Capabilities
|
|
28
|
+
|
|
29
|
+
None — internal change (no user-facing surface).
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
Docs-only: no runtime surface, no endpoint, no config key, no behaviour change. The standard's own
|
|
34
|
+
`## Decision points touched` and `## Multi-machine posture` sections are present as the spec format
|
|
35
|
+
requires.
|
|
36
|
+
|
|
37
|
+
Shipped as its own change deliberately. The document was written and staged inside an unrelated
|
|
38
|
+
worktree that is blocked awaiting operator approval — so a finished deliverable was sitting
|
|
39
|
+
uncommitted behind a blocker it has no dependency on, one worktree removal away from being lost.
|
|
40
|
+
Extracting it is the concrete application of the project's own rule that deferral is deletion.
|
|
41
|
+
|
|
42
|
+
## Known limits
|
|
43
|
+
|
|
44
|
+
This ships the standard, not its enforcement. Applying it to the first runtime consumer
|
|
45
|
+
(`PermissionPromptAutoResolver`, which today records matched-pattern names and a one-way fingerprint
|
|
46
|
+
but no replayable prompt region) is tracked separately as ACT-1312 and is deliberately NOT bundled
|
|
47
|
+
here. Until that lands, the standard is a written rule with no mechanical guard behind it — which by
|
|
48
|
+
this project's own measure means it is not yet enforced.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Side-effects review — write-trace gate-installation self-check
|
|
2
|
+
|
|
3
|
+
**Change:** `skills/instar-dev/scripts/write-trace.mjs` refuses to write a trace when the instar-dev
|
|
4
|
+
commit gate is not installed (hooksPath points at a missing directory, or no pre-commit hook exists).
|
|
5
|
+
Overridable via `INSTAR_DEV_ALLOW_UNINSTALLED_GATE=1`, and the override is recorded in the trace.
|
|
6
|
+
|
|
7
|
+
**Decision point touched?** Yes. This adds a refusal — the script now declines an operation it
|
|
8
|
+
previously always performed. Per `docs/signal-vs-authority.md` the refusal is deliberately scoped as
|
|
9
|
+
a SIGNAL: it cannot block a commit (the detected condition is precisely the one where no commit hook
|
|
10
|
+
runs), it only refuses to emit an artifact that would assert an approval that never happened.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Over-block
|
|
15
|
+
|
|
16
|
+
Real and found during the build, not theorised. The first implementation refused inside any directory
|
|
17
|
+
whose hooks were absent, which broke seven existing tests in `write-trace-tier.test.ts` and
|
|
18
|
+
`duplicate-build-guard-gates.test.ts` — both drive the script inside bare temp directories.
|
|
19
|
+
|
|
20
|
+
Resolved by scoping the check to its actual hazard: a *git repository* whose hook will not run. A
|
|
21
|
+
directory that is not a git repo has no commit to gate, so the check does not apply. Deliberately NOT
|
|
22
|
+
resolved by setting the override env var in each harness — that would push the burden onto whoever
|
|
23
|
+
writes the next test remembering to do it, which is the willpower-over-structure trade this change
|
|
24
|
+
exists to remove.
|
|
25
|
+
|
|
26
|
+
Remaining over-block risk: a repo that installs its pre-commit hook by some mechanism other than
|
|
27
|
+
`core.hooksPath` or `.git/hooks/pre-commit` would be refused incorrectly. No such mechanism is used
|
|
28
|
+
in this repo. The override exists for exactly that case and records itself.
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
The check verifies a pre-commit hook *exists*; it does not verify it is executable, non-empty, or that
|
|
33
|
+
it is actually the instar-dev gate rather than some other hook. A repo with a hook file that exits 0
|
|
34
|
+
immediately would pass this check while gating nothing. Named rather than fixed: deeper verification
|
|
35
|
+
would mean executing or parsing the hook, which is a larger change than the failure being closed.
|
|
36
|
+
|
|
37
|
+
It also cannot help after the fact. A commit already made without the gate leaves no marker, because
|
|
38
|
+
nothing ran to leave one.
|
|
39
|
+
|
|
40
|
+
## 3. Level-of-abstraction fit
|
|
41
|
+
|
|
42
|
+
Correct, and the placement is the substance of the change. The gate cannot detect its own absence —
|
|
43
|
+
an uninstalled hook cannot execute to report that it is uninstalled — so the check must live in
|
|
44
|
+
something that actually runs. Trace-writing is the nearest chokepoint the agent invokes by hand, and
|
|
45
|
+
it is the step whose whole meaning is "this change came through the skill". Putting the check
|
|
46
|
+
anywhere the agent might skip would reproduce the original problem one layer out.
|
|
47
|
+
|
|
48
|
+
## 4. Signal vs authority compliance
|
|
49
|
+
|
|
50
|
+
Compliant by construction. It holds no blocking authority over commits and could not acquire any:
|
|
51
|
+
the condition it detects is defined by the absence of the mechanism that would do the blocking. It
|
|
52
|
+
refuses to produce an artifact, which is the weakest possible action, and it fails loudly rather than
|
|
53
|
+
silently. The escape hatch is recorded rather than merely permitted, so an overridden trace is
|
|
54
|
+
distinguishable from an approved one by field presence.
|
|
55
|
+
|
|
56
|
+
## 5. Interactions
|
|
57
|
+
|
|
58
|
+
Composes with the pre-commit gate rather than duplicating it: without a trace an in-scope commit is
|
|
59
|
+
refused anyway *once the hook is installed*, so the two layers cover different halves of the same
|
|
60
|
+
failure. The trace gains two fields (`gateInstallationOverridden`, `gateInstallationReason`) emitted
|
|
61
|
+
ONLY on override, so every normal trace round-trips byte-identically and existing readers are
|
|
62
|
+
unaffected.
|
|
63
|
+
|
|
64
|
+
Adds two `git config` / `git rev-parse` subprocess calls to a script that already shells out. No hot
|
|
65
|
+
path; runs once per commit at most.
|
|
66
|
+
|
|
67
|
+
## 6. External surfaces
|
|
68
|
+
|
|
69
|
+
None. `write-trace.mjs` is a development-time script in the instar repo, not shipped behaviour, not
|
|
70
|
+
an endpoint, not agent-visible at runtime. The only observers are instar developers and CI.
|
|
71
|
+
|
|
72
|
+
## 7. Multi-machine posture
|
|
73
|
+
|
|
74
|
+
Not applicable — this is a development-time script operating on the local checkout. It introduces no
|
|
75
|
+
state, no persistence, and nothing to replicate, proxy, or reconcile. Every machine that develops
|
|
76
|
+
instar runs its own copy against its own worktree, which is the correct and only sensible posture.
|
|
77
|
+
|
|
78
|
+
## 8. Rollback cost
|
|
79
|
+
|
|
80
|
+
Trivial. Remove the check function and its call site; the trace fields disappear with it (they are
|
|
81
|
+
emitted only on override, so nothing else references them). No data migration, no persisted state, no
|
|
82
|
+
agent state. The rollback re-creates the original silent-gate hazard, so it should be accompanied by
|
|
83
|
+
a reason.
|