@nurix/etna 0.19.79 → 0.19.81
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/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/awareness.mjs +14 -5
- package/data/harnesses/code/hooks/compliance.mjs +18 -5
- package/data/harnesses/code/hooks/lib/approved.mjs +32 -0
- package/data/harnesses/code/rules/general.md +10 -2
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/rules/general.md +10 -2
- package/package.json +2 -2
package/data/harness-index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.10.
|
|
4
|
+
"version": "3.10.7",
|
|
5
5
|
"files": [
|
|
6
6
|
".prettierignore",
|
|
7
7
|
".prettierrc.json",
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
"docs/memory/main.md",
|
|
16
16
|
".mcp.json"
|
|
17
17
|
],
|
|
18
|
-
"digest": "
|
|
18
|
+
"digest": "3e27328dde24"
|
|
19
19
|
}
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
recordTouched,
|
|
32
32
|
} from "./lib/state.mjs";
|
|
33
33
|
import { frozenStatus } from "./lib/frozen.mjs";
|
|
34
|
-
import { approvedPackages } from "./lib/approved.mjs";
|
|
34
|
+
import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
|
|
35
35
|
import {
|
|
36
36
|
recordSession,
|
|
37
37
|
liveSiblings,
|
|
@@ -484,15 +484,24 @@ function detectOutcomes(projectDir, meta, rows) {
|
|
|
484
484
|
} catch (e) {
|
|
485
485
|
continue;
|
|
486
486
|
}
|
|
487
|
+
// A row is raised for a *rejected* package only — a decision already taken and violated.
|
|
488
|
+
// A package with no decision either way is not a defect: it is a dimension nobody has ruled
|
|
489
|
+
// on, harvested centrally by the fleet package census. Raising rows on absence turned the
|
|
490
|
+
// commit wall into a tax on every unlisted dependency and produced waivers, not decisions.
|
|
491
|
+
const rejected = rejectedPackages(projectDir);
|
|
487
492
|
const names = [...Object.keys(manifest.dependencies || {}), ...Object.keys(manifest.devDependencies || {})];
|
|
488
493
|
for (const name of names) {
|
|
489
|
-
if (approved.has(name) || raised(`\`${name}\` (package-
|
|
494
|
+
if (approved.has(name) || raised(`\`${name}\` (package-rejected`)) continue;
|
|
495
|
+
const no = rejected.get(name);
|
|
496
|
+
if (!no) continue;
|
|
490
497
|
rows.push(
|
|
491
498
|
formatAutoRow({
|
|
492
499
|
...meta,
|
|
493
|
-
kind: "package-
|
|
494
|
-
description: `${rel} declares \`${name}\` (package-
|
|
495
|
-
action:
|
|
500
|
+
kind: "package-rejected",
|
|
501
|
+
description: `${rel} declares \`${name}\` (package-rejected), which the approved table rejects`,
|
|
502
|
+
action: no.use
|
|
503
|
+
? `replace it with \`${no.use}\`, or change the decision in .claude/hooks/approved-packages.md`
|
|
504
|
+
: `use the choice ${no.rule} prescribes for this stack, or change the decision`,
|
|
496
505
|
})
|
|
497
506
|
);
|
|
498
507
|
}
|
|
@@ -13,7 +13,7 @@ import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state
|
|
|
13
13
|
import { frozenStatus } from "./lib/frozen.mjs";
|
|
14
14
|
import { undischargedRows } from "./lib/ledger.mjs";
|
|
15
15
|
import { appendEvent, unresolvedDenials, sessionLogPath, malformedEventLines } from "./lib/session-log.mjs";
|
|
16
|
-
import { approvedPackages } from "./lib/approved.mjs";
|
|
16
|
+
import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
|
|
17
17
|
import { claimActivity, releaseActivity, recentForeignTouches } from "./lib/claims.mjs";
|
|
18
18
|
|
|
19
19
|
function readSafe(file) {
|
|
@@ -277,6 +277,7 @@ function packageGate(projectDir, command, sessionId) {
|
|
|
277
277
|
if (!m) return null;
|
|
278
278
|
const approved = approvedPackages(projectDir);
|
|
279
279
|
if (approved.size === 0) return null; // no rules file installed — nothing to enforce
|
|
280
|
+
const rejected = rejectedPackages(projectDir);
|
|
280
281
|
const raw = m[1] || "";
|
|
281
282
|
// Test packages install at `latest`, outside the table's pin set (.claude/rules/testing.md).
|
|
282
283
|
// Two dev-only installs are a decision already taken rather than a new one: a companion in an
|
|
@@ -291,16 +292,25 @@ function packageGate(projectDir, command, sessionId) {
|
|
|
291
292
|
const args = raw.split(/\s+/).filter((a) => a && !a.startsWith("-") && PKG_SPEC.test(a));
|
|
292
293
|
for (const arg of args) {
|
|
293
294
|
const pkg = arg.startsWith("@") ? arg.split("@").slice(0, 2).join("@") : arg.split("@")[0];
|
|
295
|
+
if (approved.has(pkg)) continue;
|
|
294
296
|
if (isDev && pkg.startsWith("@") && approvedScopes.has(pkg.split("/")[0])) continue;
|
|
295
|
-
|
|
297
|
+
// Approved wins over rejected: `jest` is banned for web and Express and prescribed for Expo,
|
|
298
|
+
// so a name in both is a prescription somewhere and must not be denied.
|
|
299
|
+
const no = rejected.get(pkg);
|
|
300
|
+
if (no) {
|
|
301
|
+
const instead = no.use
|
|
302
|
+
? `Use \`${no.use}\`${no.version ? ` ${no.version}` : ""}${no.purpose ? ` — ${no.purpose}` : ""}.`
|
|
303
|
+
: `The prescribed choice for your stack is in ${no.rule}.`;
|
|
296
304
|
return deny(
|
|
297
305
|
projectDir,
|
|
298
306
|
sessionId,
|
|
299
307
|
"package-table",
|
|
300
308
|
pkg,
|
|
301
|
-
`BLOCKED —
|
|
309
|
+
`BLOCKED — \`${pkg}\` is rejected here. ${instead} Rejections are recorded in .claude/hooks/approved-packages.md; changing one is an explicit user decision.`
|
|
302
310
|
);
|
|
303
311
|
}
|
|
312
|
+
// No decision exists for this package — not approved, not rejected. Allowed, and harvested
|
|
313
|
+
// centrally by the fleet package census rather than blocked here.
|
|
304
314
|
}
|
|
305
315
|
// Every named package is approved — a package-table denial for one of them is a successful retry.
|
|
306
316
|
for (const arg of args) {
|
|
@@ -313,15 +323,18 @@ function packageGate(projectDir, command, sessionId) {
|
|
|
313
323
|
// The repo a Bash command acts on: an explicit `cd <dir>` in the command wins, then the
|
|
314
324
|
// payload's cwd, then the session's home project — a session may operate across repos
|
|
315
325
|
// (forge + consumer), and gating a commit against the wrong repo blocks or passes falsely.
|
|
326
|
+
// A candidate is normalized to its git toplevel: a cwd inside the repo (packages/studio)
|
|
327
|
+
// still gates — and writes the session log — against the repo root, not the subdirectory.
|
|
316
328
|
function commandRoot(projectDir, payload, command) {
|
|
317
329
|
const cdMatch = /(?:^|&&|;)\s*cd\s+"?([^"\s;&]+)"?/.exec(command || "");
|
|
318
330
|
for (const candidate of [cdMatch && cdMatch[1], payload.cwd]) {
|
|
319
331
|
if (!candidate || !path.isAbsolute(candidate)) continue;
|
|
320
332
|
try {
|
|
321
|
-
if (fs.statSync(candidate).isDirectory())
|
|
333
|
+
if (!fs.statSync(candidate).isDirectory()) continue;
|
|
322
334
|
} catch (e) {
|
|
323
|
-
|
|
335
|
+
continue;
|
|
324
336
|
}
|
|
337
|
+
return git(candidate, ["rev-parse", "--show-toplevel"]) || candidate;
|
|
325
338
|
}
|
|
326
339
|
return projectDir;
|
|
327
340
|
}
|
|
@@ -56,3 +56,35 @@ export function approvedPackages(projectDir) {
|
|
|
56
56
|
}
|
|
57
57
|
return names;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
// A rejection is a decision already taken, and it names its replacement: the approved table's rows
|
|
61
|
+
// are `| package | version | use | don't use |`, so every name in a Don't-use cell points at that
|
|
62
|
+
// row's package. testing.md's framework table bans by stack without a single substitute, so those
|
|
63
|
+
// rejections carry the rule reference instead. Absence from both maps is not a rejection — it is a
|
|
64
|
+
// dimension nobody has ruled on, and the gate lets it through (the census harvests it centrally).
|
|
65
|
+
export function rejectedPackages(projectDir) {
|
|
66
|
+
const rejected = new Map();
|
|
67
|
+
const table = path.join(projectDir, ".claude", "hooks", "approved-packages.md");
|
|
68
|
+
for (const line of readSafe(table).split("\n")) {
|
|
69
|
+
const cells = line.split("|");
|
|
70
|
+
if (cells.length < 6) continue;
|
|
71
|
+
const pkg = /`([^`]+)`/.exec(cells[1] || "");
|
|
72
|
+
if (!pkg || pkg[1] === "Package" || pkg[1] === "---") continue;
|
|
73
|
+
const version = (/`?([^`|]+)`?/.exec((cells[2] || "").trim()) || [])[1] || "";
|
|
74
|
+
for (const m of (cells[4] || "").matchAll(/`(@?[a-z0-9][\w./-]*)`/g)) {
|
|
75
|
+
if (m[1] === pkg[1]) continue; // "`mermaid` via CDN" bans a delivery, not the row's own package
|
|
76
|
+
rejected.set(m[1], { use: pkg[1], version: version.trim(), purpose: (cells[3] || "").trim() });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const testing = path.join(projectDir, ".claude", "rules", "testing.md");
|
|
80
|
+
let banColumn = -1;
|
|
81
|
+
for (const line of readSafe(testing).split("\n")) {
|
|
82
|
+
banColumn = banColumnOf(line, banColumn);
|
|
83
|
+
if (banColumn < 0 || !line.trimStart().startsWith("|")) continue;
|
|
84
|
+
const cell = line.split("|")[banColumn] || "";
|
|
85
|
+
for (const m of cell.matchAll(/`(@?[a-z0-9][\w./-]*)`/g)) {
|
|
86
|
+
if (!rejected.has(m[1])) rejected.set(m[1], { rule: "testing.md § Frameworks by stack" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return rejected;
|
|
90
|
+
}
|
|
@@ -33,9 +33,17 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
|
|
|
33
33
|
|
|
34
34
|
## Packages
|
|
35
35
|
|
|
36
|
-
**
|
|
36
|
+
**The approved table is `.claude/hooks/approved-packages.md`**, enforced by the PreToolUse install gate. It records three states, and the gate treats them differently:
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
| The package is… | What happens |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| in the **Package** column | installs |
|
|
41
|
+
| in a **Don't use** column | **blocked**, and the block names that row's replacement — `moment` answers "use `date-fns`" |
|
|
42
|
+
| in neither | **installs.** No decision exists, so there is nothing to enforce |
|
|
43
|
+
|
|
44
|
+
A rejection is a decision and binds; an absence is not one and never blocks work. Unlisted packages are harvested across the fleet by the package census, and become rows — or bans — by explicit user decision. **Never edit the table to unblock yourself**; a rejection changes only on the user's say-so.
|
|
45
|
+
|
|
46
|
+
Two **dev-only** installs are exempt even from the rejection check being reached: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`.
|
|
39
47
|
|
|
40
48
|
## API Response Envelope
|
|
41
49
|
|
|
@@ -33,9 +33,17 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
|
|
|
33
33
|
|
|
34
34
|
## Packages
|
|
35
35
|
|
|
36
|
-
**
|
|
36
|
+
**The approved table is `.claude/hooks/approved-packages.md`**, enforced by the PreToolUse install gate. It records three states, and the gate treats them differently:
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
| The package is… | What happens |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| in the **Package** column | installs |
|
|
41
|
+
| in a **Don't use** column | **blocked**, and the block names that row's replacement — `moment` answers "use `date-fns`" |
|
|
42
|
+
| in neither | **installs.** No decision exists, so there is nothing to enforce |
|
|
43
|
+
|
|
44
|
+
A rejection is a decision and binds; an absence is not one and never blocks work. Unlisted packages are harvested across the fleet by the package census, and become rows — or bans — by explicit user decision. **Never edit the table to unblock yourself**; a rejection changes only on the user's say-so.
|
|
45
|
+
|
|
46
|
+
Two **dev-only** installs are exempt even from the rejection check being reached: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`.
|
|
39
47
|
|
|
40
48
|
## API Response Envelope
|
|
41
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.81",
|
|
4
4
|
"description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"agent",
|
|
43
43
|
"plugin"
|
|
44
44
|
],
|
|
45
|
-
"etnaFingerprint": "
|
|
45
|
+
"etnaFingerprint": "798dd0b4e94c"
|
|
46
46
|
}
|