@xhulz/nina 0.38.0 → 0.40.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/package.json +1 -1
- package/releases/0.39.0/core/GAPS.md +125 -0
- package/releases/0.39.0/core/answered.json +32 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.39.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.39.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.39.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.39.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.39.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.39.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.39.0/core/tree/.claude/agents/qa.md +152 -0
- package/releases/0.39.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.39.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.39.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.39.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.39.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.39.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.39.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.39.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.39.0/core/tree/.claude/pills/README.md +147 -0
- package/releases/0.39.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.39.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.39.0/core/tree/.claude/router.md +198 -0
- package/releases/0.39.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.39.0/core/tree/CLAUDE.md +163 -0
- package/releases/0.39.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.39.0/core/tree/scripts/edit-guard.mjs +24 -0
- package/releases/0.39.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.39.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.39.0/core/vocabulary.json +13 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.39.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.39.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.39.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.39.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.39.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.39.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.39.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.39.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.39.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.39.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.39.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.39.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.39.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.39.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/releases/0.40.0/core/GAPS.md +125 -0
- package/releases/0.40.0/core/answered.json +32 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.40.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.40.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.40.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.40.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.40.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.40.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.40.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.40.0/core/tree/.claude/agents/qa.md +152 -0
- package/releases/0.40.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.40.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.40.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.40.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.40.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.40.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.40.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.40.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.40.0/core/tree/.claude/pills/README.md +147 -0
- package/releases/0.40.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.40.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.40.0/core/tree/.claude/router.md +198 -0
- package/releases/0.40.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.40.0/core/tree/CLAUDE.md +163 -0
- package/releases/0.40.0/core/tree/scripts/cost-watch.mjs +25 -0
- package/releases/0.40.0/core/tree/scripts/edit-guard.mjs +24 -0
- package/releases/0.40.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.40.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.40.0/core/vocabulary.json +13 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.40.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.40.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.40.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.40.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.40.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.40.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.40.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.40.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.40.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.40.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.40.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.40.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.40.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.40.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.40.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.40.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.40.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.40.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.40.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/check.mjs +15 -28
- package/src/commands/compose.mjs +50 -64
- package/src/commands/eval.mjs +1 -1
- package/src/commands/export.mjs +4 -34
- package/src/commands/gate.mjs +4 -11
- package/src/commands/init.mjs +22 -30
- package/src/commands/langfuse.mjs +1 -1
- package/src/commands/learn.mjs +10 -12
- package/src/commands/pills.mjs +5 -5
- package/src/commands/pipeline.mjs +4 -8
- package/src/commands/release.mjs +2 -3
- package/src/commands/runs.mjs +1 -1
- package/src/commands/snapshot.mjs +12 -50
- package/src/commands/stats.mjs +7 -8
- package/src/commands/status.mjs +4 -5
- package/src/commands/upgrade.mjs +9 -12
- package/src/commands/where.mjs +9 -18
- package/src/commands/wire.mjs +8 -19
- package/src/deep.mjs +3 -3
- package/src/gate.mjs +54 -7
- package/src/graph.mjs +1 -1
- package/src/guard.mjs +194 -7
- package/src/langfuse.mjs +9 -8
- package/src/lock.mjs +82 -0
- package/src/paths.mjs +65 -0
- package/src/shell.mjs +3 -3
- package/src/steps.mjs +5 -19
- package/src/store.mjs +2 -2
- package/src/wiring.mjs +20 -7
package/src/guard.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync }
|
|
|
21
21
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { NOTICE_HEAD, composedPaths, noticeOf } from './commands/compose.mjs';
|
|
24
|
-
import { HARNESS } from './paths.mjs';
|
|
24
|
+
import { HARNESS, layerRootFor, readProfile } from './paths.mjs';
|
|
25
25
|
|
|
26
26
|
// Read here and by the suites from here; the test itself lives beside the compiler that writes the notice.
|
|
27
27
|
export { NOTICE_HEAD, noticeOf };
|
|
@@ -32,6 +32,190 @@ const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
32
32
|
/** The tools that change a file, and where each names it. */
|
|
33
33
|
const EDITS = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' };
|
|
34
34
|
|
|
35
|
+
/** The git subcommands that move the checkout or write its history — Hard Rule #18's. */
|
|
36
|
+
const MOVES = new Set(['stash', 'checkout', 'switch', 'reset', 'restore', 'clean', 'commit', 'merge', 'rebase', 'cherry-pick', 'revert', 'am', 'apply', 'pull', 'push']);
|
|
37
|
+
|
|
38
|
+
/** Words before the command word that leave it the command: `env X=1 git …`, `command git …`, `{ git …; }`. */
|
|
39
|
+
const RUNNERS = new Set(['env', 'command', 'exec', 'time', 'nohup', 'nice', '{', '}', '!']);
|
|
40
|
+
|
|
41
|
+
/** A `-C` or a `cd` to one of these is still the tree the stage was dispatched into. */
|
|
42
|
+
const HERE = new Set(['.', './']);
|
|
43
|
+
|
|
44
|
+
/** Where a heredoc's delimiter word ends. */
|
|
45
|
+
const DELIMITER_ENDS = new Set([' ', '\t', '\r', '\n', ';', '&', '|', '(', ')', '`', '<', '>']);
|
|
46
|
+
|
|
47
|
+
/** The characters that can end a run of plain word characters. */
|
|
48
|
+
const SPECIAL = new Set(["'", '"', '\\', '#', '\n', '<', '&', ';', '|', '(', ')', '`', ' ', '\t', '\r']);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A shell command as the shell reads it: its simple commands, in order, each as its words with the quotes removed.
|
|
52
|
+
* `&&`, `||`, `;`, `|`, `&`, a newline, a parenthesis and a backquote end one; a quoted string is part of a word — `"git" stash`
|
|
53
|
+
* is git — and never a separator or a command of its own, across lines too; a comment and a heredoc's body are not
|
|
54
|
+
* read. One pass, no pattern: the command is a model's, and a pattern that backtracks on it is one a stage could
|
|
55
|
+
* stall the guard with.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} command
|
|
58
|
+
* @returns {string[][]}
|
|
59
|
+
*/
|
|
60
|
+
function simpleCommands(command) {
|
|
61
|
+
const text = String(command);
|
|
62
|
+
const commands = [[]];
|
|
63
|
+
const heredocs = [];
|
|
64
|
+
let word = null;
|
|
65
|
+
const end = () => {
|
|
66
|
+
if (word !== null) commands.at(-1).push(word);
|
|
67
|
+
word = null;
|
|
68
|
+
};
|
|
69
|
+
const next = () => {
|
|
70
|
+
end();
|
|
71
|
+
if (commands.at(-1).length) commands.push([]);
|
|
72
|
+
};
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < text.length) {
|
|
75
|
+
const c = text[i];
|
|
76
|
+
if (c === "'") {
|
|
77
|
+
const close = text.indexOf("'", i + 1);
|
|
78
|
+
const stop = close === -1 ? text.length : close;
|
|
79
|
+
word = (word ?? '') + text.slice(i + 1, stop);
|
|
80
|
+
i = stop + 1;
|
|
81
|
+
} else if (c === '"') {
|
|
82
|
+
const parts = [];
|
|
83
|
+
i += 1;
|
|
84
|
+
let from = i;
|
|
85
|
+
while (i < text.length && text[i] !== '"') {
|
|
86
|
+
if (text[i] === '\\' && i + 1 < text.length) {
|
|
87
|
+
parts.push(text.slice(from, i));
|
|
88
|
+
from = i + 1;
|
|
89
|
+
i += 2;
|
|
90
|
+
} else i += 1;
|
|
91
|
+
}
|
|
92
|
+
parts.push(text.slice(from, i));
|
|
93
|
+
word = (word ?? '') + parts.join('');
|
|
94
|
+
i += 1;
|
|
95
|
+
} else if (c === '\\') {
|
|
96
|
+
if (text[i + 1] !== '\n') word = (word ?? '') + (text[i + 1] ?? '');
|
|
97
|
+
i += 2;
|
|
98
|
+
} else if (c === '#' && word === null) {
|
|
99
|
+
const line = text.indexOf('\n', i);
|
|
100
|
+
i = line === -1 ? text.length : line;
|
|
101
|
+
} else if (c === '\n') {
|
|
102
|
+
next();
|
|
103
|
+
i += 1;
|
|
104
|
+
for (const delimiter of heredocs.splice(0)) {
|
|
105
|
+
while (i < text.length) {
|
|
106
|
+
const line = text.indexOf('\n', i);
|
|
107
|
+
const stop = line === -1 ? text.length : line;
|
|
108
|
+
const body = text.slice(i, stop);
|
|
109
|
+
i = stop + 1;
|
|
110
|
+
if (body.trim() === delimiter) break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (c === '<' && text.startsWith('<<<', i)) {
|
|
114
|
+
word = (word ?? '') + '<<<';
|
|
115
|
+
i += 3;
|
|
116
|
+
} else if (c === '<' && text[i + 1] === '<') {
|
|
117
|
+
end();
|
|
118
|
+
i += text[i + 2] === '-' ? 3 : 2;
|
|
119
|
+
while (text[i] === ' ' || text[i] === '\t') i += 1;
|
|
120
|
+
const from = i;
|
|
121
|
+
while (i < text.length && !DELIMITER_ENDS.has(text[i])) i += 1;
|
|
122
|
+
const delimiter = text.slice(from, i).replaceAll("'", '').replaceAll('"', '').replaceAll('\\', '');
|
|
123
|
+
if (delimiter) heredocs.push(delimiter);
|
|
124
|
+
} else if (c === '&' && (text[i + 1] === '>' || word?.endsWith('>') || word?.endsWith('<'))) {
|
|
125
|
+
word = (word ?? '') + c;
|
|
126
|
+
i += 1;
|
|
127
|
+
} else if (c === ';' || c === '&' || c === '|' || c === '(' || c === ')' || c === '`') {
|
|
128
|
+
next();
|
|
129
|
+
i += (c === '&' || c === '|') && text[i + 1] === c ? 2 : 1;
|
|
130
|
+
} else if (c === ' ' || c === '\t' || c === '\r') {
|
|
131
|
+
end();
|
|
132
|
+
i += 1;
|
|
133
|
+
} else {
|
|
134
|
+
const from = i;
|
|
135
|
+
i += 1;
|
|
136
|
+
while (i < text.length && !SPECIAL.has(text[i])) i += 1;
|
|
137
|
+
word = (word ?? '') + text.slice(from, i);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
end();
|
|
141
|
+
return commands.filter((words) => words.length);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The git subcommand in a shell command that would move the checkout the command runs in, or null. Read command
|
|
146
|
+
* by command, in order, as the shell runs it: a `cd` exempts only what comes after it — `git stash && cd /tmp`
|
|
147
|
+
* stashed the tree all the same, and a test of the whole string for a `cd` let it through — and `-C .` is not
|
|
148
|
+
* another directory. A git command is one whose command word is `git`, not a phrase in a quoted string or a
|
|
149
|
+
* heredoc's body; its subcommand is the word after its global options, so `merge-base` is not `merge`; and a
|
|
150
|
+
* stash's `list` or `show`, and an `apply` that only checks, read.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} command - A Bash tool call's command.
|
|
153
|
+
* @returns {string|null}
|
|
154
|
+
*/
|
|
155
|
+
export function checkoutMove(command) {
|
|
156
|
+
let moved = false;
|
|
157
|
+
for (const words of simpleCommands(command)) {
|
|
158
|
+
while (words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || RUNNERS.has(words[0]))) words.shift();
|
|
159
|
+
if (words[0] === 'cd' || words[0] === 'pushd') {
|
|
160
|
+
moved ||= !HERE.has(words[1] ?? '');
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (words[0] !== 'git') continue;
|
|
164
|
+
let i = 1;
|
|
165
|
+
let elsewhere = false;
|
|
166
|
+
while (words[i]?.startsWith('-')) {
|
|
167
|
+
if (words[i] === '-C') {
|
|
168
|
+
elsewhere ||= !HERE.has(words[i + 1] ?? '.');
|
|
169
|
+
i += 2;
|
|
170
|
+
} else if (words[i] === '-c') {
|
|
171
|
+
i += 2;
|
|
172
|
+
} else {
|
|
173
|
+
if (/^--(git-dir|work-tree)\b/.test(words[i])) elsewhere = true;
|
|
174
|
+
i += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const sub = words[i];
|
|
178
|
+
if (!MOVES.has(sub) || moved || elsewhere) continue;
|
|
179
|
+
const rest = words.slice(i + 1);
|
|
180
|
+
if (sub === 'stash' && ['list', 'show'].includes(rest[0])) continue;
|
|
181
|
+
if (sub === 'apply' && rest.some((w) => ['--check', '--stat', '--numstat', '--summary'].includes(w)) && !rest.includes('--apply')) continue;
|
|
182
|
+
return sub;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A stage's shell command that would move the checkout it was dispatched into, refused. Only the orchestrator
|
|
189
|
+
* commits, and the work in flight lives in that tree, uncommitted: one reviewer, told not to edit, stashed it to
|
|
190
|
+
* compare with the baseline, and its pop failed on a conflict.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} input - The hook's stdin, parsed.
|
|
193
|
+
* @param {{layers: string}} pinned - The pinned layers, whose hard rules say whether this one is the project's.
|
|
194
|
+
* @returns {object|null}
|
|
195
|
+
*/
|
|
196
|
+
function handleShell(input, pinned) {
|
|
197
|
+
if (!input.agent_id) return null;
|
|
198
|
+
const moves = checkoutMove(input.tool_input?.command ?? '');
|
|
199
|
+
if (!moves) return null;
|
|
200
|
+
let rules = '';
|
|
201
|
+
try {
|
|
202
|
+
rules = readFileSync(join(pinned.layers, 'core', 'tree', 'CLAUDE.md'), 'utf8');
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (!rules.includes('Only the orchestrator commits')) return null;
|
|
207
|
+
return {
|
|
208
|
+
hookSpecificOutput: {
|
|
209
|
+
hookEventName: 'PreToolUse',
|
|
210
|
+
permissionDecision: 'deny',
|
|
211
|
+
permissionDecisionReason:
|
|
212
|
+
`NINA: \`git ${moves}\` would move the checkout the pipeline's work in flight lives in — Hard Rule #18: only the orchestrator ` +
|
|
213
|
+
'commits, and no stage moves the checkout. Read history with git diff, log, show, status or blame, and build or test another ' +
|
|
214
|
+
'revision in a `git worktree` you remove afterwards.',
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
35
219
|
/** How much of a file is read to find it: the head, not the whole of a large file on every edit. */
|
|
36
220
|
const HEAD_BYTES = 16 * 1024;
|
|
37
221
|
|
|
@@ -43,13 +227,12 @@ const HEAD_BYTES = 16 * 1024;
|
|
|
43
227
|
* @param {string} [pkg] - The NINA install to look in.
|
|
44
228
|
* @returns {{layers: string, surfaces: string[]}|null}
|
|
45
229
|
*/
|
|
46
|
-
|
|
230
|
+
function pinnedLayers(root, pkg = PACKAGE) {
|
|
47
231
|
try {
|
|
48
|
-
const profile =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return { layers, surfaces: Array.isArray(profile.surfaces) ? profile.surfaces : [] };
|
|
232
|
+
const { profile } = readProfile(root);
|
|
233
|
+
const layers = profile ? layerRootFor(pkg, profile.core) : { error: 'no profile' };
|
|
234
|
+
if (layers.error || !existsSync(join(layers.dir, 'core', 'tree', 'scripts', 'edit-guard.mjs'))) return null;
|
|
235
|
+
return { layers: layers.dir, surfaces: Array.isArray(profile.surfaces) ? profile.surfaces : [] };
|
|
53
236
|
} catch {
|
|
54
237
|
return null;
|
|
55
238
|
}
|
|
@@ -83,6 +266,10 @@ function head(path) {
|
|
|
83
266
|
export async function handleEdit(input, { root, pkg = PACKAGE }) {
|
|
84
267
|
try {
|
|
85
268
|
if (input?.hook_event_name !== 'PreToolUse') return null;
|
|
269
|
+
if (input.tool_name === 'Bash') {
|
|
270
|
+
const pinnedShell = input.agent_id ? pinnedLayers(root, pkg) : null;
|
|
271
|
+
return pinnedShell ? handleShell(input, pinnedShell) : null;
|
|
272
|
+
}
|
|
86
273
|
const key = EDITS[input.tool_name];
|
|
87
274
|
const path = key ? input.tool_input?.[key] : null;
|
|
88
275
|
const pinned = typeof path === 'string' ? pinnedLayers(root, pkg) : null;
|
package/src/langfuse.mjs
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Observations go over OpenTelemetry (`/api/public/otel/v1/traces`, OTLP/HTTP as JSON), the path
|
|
15
15
|
* Langfuse's v4 data model takes, with the session and trace attributes on every span, as its guide asks.
|
|
16
|
-
* Verdicts go to `/api/public/
|
|
16
|
+
* Verdicts go as categorical scores, in batches to `/api/public/ingestion`, or one at a time to
|
|
17
|
+
* `/api/public/scores` where the batch endpoint is gone. Langfuse keeps what it was first sent: the
|
|
17
18
|
* same span id sent again is a second observation, not an update, and every sum counts both; a score is
|
|
18
|
-
* replaced only when its id, name and date all match, and
|
|
19
|
-
* day it is called. So each is sent once, and only for a run that has settled. No dependency: `fetch`
|
|
19
|
+
* replaced only when its id, name and date all match, and the scores endpoint takes no date — it stamps
|
|
20
|
+
* the day it is called. So each is sent once, and only for a run that has settled. No dependency: `fetch`
|
|
20
21
|
* is enough.
|
|
21
22
|
*/
|
|
22
23
|
|
|
@@ -40,13 +41,13 @@ export const BATCH = 100;
|
|
|
40
41
|
export const SETTLE_MINUTES = 15;
|
|
41
42
|
|
|
42
43
|
/** How long a run that never returned is waited for before it is sent as it is. */
|
|
43
|
-
|
|
44
|
+
const UNRETURNED_HOURS = 24;
|
|
44
45
|
|
|
45
46
|
/** A stable hex id of a given length, from what it identifies. */
|
|
46
47
|
const hexId = (text, length) => createHash('sha256').update(String(text)).digest('hex').slice(0, length);
|
|
47
48
|
|
|
48
49
|
/** The trace a dispatch becomes, and the observation at its root. */
|
|
49
|
-
|
|
50
|
+
const traceIdOf = (record) => hexId(`trace|${record.dispatch_id}`, 32);
|
|
50
51
|
export const spanIdOf = (record) => hexId(record.dispatch_id, 16);
|
|
51
52
|
|
|
52
53
|
/** An ISO time, as OTLP's nanoseconds since the epoch — a string, since it outgrows a double. */
|
|
@@ -230,7 +231,7 @@ export function scoreOf(record) {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/** The OTLP request body for a batch of spans. */
|
|
233
|
-
|
|
234
|
+
function otlpBody(spans) {
|
|
234
235
|
return {
|
|
235
236
|
resourceSpans: [
|
|
236
237
|
{
|
|
@@ -247,8 +248,8 @@ export function otlpBody(spans) {
|
|
|
247
248
|
* request cut off after Langfuse took it is the one way a span goes twice, so a batch of spans, the one
|
|
248
249
|
* request that carries real weight, gets the longer wait.
|
|
249
250
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
const REQUEST_MS = 30_000;
|
|
252
|
+
const SPANS_MS = 120_000;
|
|
252
253
|
|
|
253
254
|
/** The headers every request carries. */
|
|
254
255
|
const headersFor = ({ publicKey, secretKey }) => ({
|
package/src/lock.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lock file that names the process holding it, for the commands that must not run twice at once on one
|
|
3
|
+
* project: two snapshots would leave one's cursors beside the other's records, two exports would send the same
|
|
4
|
+
* runs twice.
|
|
5
|
+
*
|
|
6
|
+
* The file appears with the pid already in it — written aside, then linked into place, which fails when a lock
|
|
7
|
+
* is there — so it is never read empty and taken for a dead one's. A lock whose process died is removed only by
|
|
8
|
+
* the taker holding its takeover, and only once it has read the holder again under it: each taker removed it
|
|
9
|
+
* outright before, after its own read, and the second removed the lock the first had just taken, so both held it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { link, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
|
+
|
|
15
|
+
/** Whether a process is running. One this user may not signal is running too. */
|
|
16
|
+
function alive(pid) {
|
|
17
|
+
if (!(pid > 0)) return false;
|
|
18
|
+
try {
|
|
19
|
+
return process.kill(pid, 0);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return error.code === 'EPERM';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The pid a lock file names — 0 when it names none — or undefined when there is no file. */
|
|
26
|
+
async function holder(path) {
|
|
27
|
+
try {
|
|
28
|
+
return Number(await readFile(path, 'utf8')) || 0;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT') return undefined;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A name beside the lock no other taker uses, in this process or another. */
|
|
36
|
+
const aside = (path) => `${path}.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
37
|
+
|
|
38
|
+
/** Puts a file naming this process at `path`, unless one is there. */
|
|
39
|
+
async function place(path) {
|
|
40
|
+
const mine = aside(path);
|
|
41
|
+
await writeFile(mine, String(process.pid));
|
|
42
|
+
try {
|
|
43
|
+
await link(mine, path);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== 'EEXIST') throw error;
|
|
47
|
+
return false;
|
|
48
|
+
} finally {
|
|
49
|
+
await rm(mine, { force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Takes a lock, or says it is held.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} path - The lock file.
|
|
57
|
+
* @returns {Promise<(() => Promise<void>)|null>} Its release, or null when a running process holds it.
|
|
58
|
+
*/
|
|
59
|
+
export async function takeLock(path) {
|
|
60
|
+
await mkdir(dirname(path), { recursive: true });
|
|
61
|
+
const takeover = `${path}.takeover`;
|
|
62
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
63
|
+
if (await place(path)) return () => rm(path, { force: true });
|
|
64
|
+
if (alive(await holder(path))) return null;
|
|
65
|
+
if (!(await place(takeover))) {
|
|
66
|
+
const taker = await holder(takeover);
|
|
67
|
+
if (alive(taker)) return null;
|
|
68
|
+
// A taker that died mid-takeover. One that finished is simply gone: try again.
|
|
69
|
+
if (taker !== undefined) await rm(takeover, { force: true });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
// Under the takeover nobody else removes the lock, and nobody can link over it: a lock read now stays what
|
|
74
|
+
// it is. A missing one does not — a taker may link it in the next instant — so there is nothing to remove.
|
|
75
|
+
const pid = await holder(path);
|
|
76
|
+
if (pid !== undefined && !alive(pid)) await rm(path, { force: true });
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(takeover, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
package/src/paths.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { readdir } from 'node:fs/promises';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
14
|
import { join, resolve } from 'node:path';
|
|
14
15
|
|
|
@@ -101,6 +102,70 @@ export function legacyHint(target) {
|
|
|
101
102
|
: '';
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A project's profile, read the one way every command reads it: `{profile, path}`, or `{path, error}` — one
|
|
107
|
+
* line saying what is wrong, with `missing` when there is no profile at all. Commands that parsed it on
|
|
108
|
+
* their own answered a stray comma with a stack trace, and each that did not said so differently.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} target - The project.
|
|
111
|
+
* @returns {{path: string, profile?: object, error?: string, missing?: boolean}}
|
|
112
|
+
*/
|
|
113
|
+
export function readProfile(target) {
|
|
114
|
+
const path = join(target, HARNESS, 'profile.json');
|
|
115
|
+
if (!existsSync(path)) {
|
|
116
|
+
const hint = legacyHint(target) || ' — run `nina init` first, or name the project with --project';
|
|
117
|
+
return { path, missing: true, error: `no ${HARNESS}/profile.json under ${target}${hint}` };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const profile = JSON.parse(readFileSync(path, 'utf8'));
|
|
121
|
+
if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) return { path, error: `${path} is not a JSON object` };
|
|
122
|
+
return { path, profile };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { path, error: `${path} cannot be read — ${error.message}` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Where a project's layers are read from.
|
|
130
|
+
*
|
|
131
|
+
* A project pins a frozen release, or tracks the working tree with "dev". A pinned release
|
|
132
|
+
* that is missing is an error, never a silent fall back to whatever the harness happens to
|
|
133
|
+
* look like right now: that would compose an unreviewed core into a project that asked for
|
|
134
|
+
* a reviewed one, and nothing downstream would say so. Every reader asks this one: the commands,
|
|
135
|
+
* and the hooks that ask whether the pin ships them.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} root - The NINA install directory.
|
|
138
|
+
* @param {string|undefined} core - The profile's `core` field.
|
|
139
|
+
* @returns {{dir: string}|{error: string}}
|
|
140
|
+
*/
|
|
141
|
+
export function layerRootFor(root, core) {
|
|
142
|
+
if (!core || core === 'dev') {
|
|
143
|
+
// `dev` means "track the layers as they are being worked on", which only exists in a
|
|
144
|
+
// checkout of this repo. An installed package ships frozen releases and nothing else,
|
|
145
|
+
// so say that rather than composing an empty tree and reporting nothing wrong.
|
|
146
|
+
if (!existsSync(join(root, 'core'))) {
|
|
147
|
+
return { error: `profile pins core "dev", which tracks the NINA working tree — ${root} is an installed package and has only releases` };
|
|
148
|
+
}
|
|
149
|
+
return { dir: root };
|
|
150
|
+
}
|
|
151
|
+
// A name, never a path: `../x` would read layers from outside the releases.
|
|
152
|
+
if (typeof core !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(core)) return { error: `profile pins core ${JSON.stringify(core)}, which is not a release name` };
|
|
153
|
+
const dir = join(root, 'releases', core);
|
|
154
|
+
if (!existsSync(dir)) return { error: `profile pins core ${core}, which is not in ${join(root, 'releases')}` };
|
|
155
|
+
return { dir };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The surfaces a layer root offers, by name, in order. Seven commands listed the directory each on its own.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} layerRoot - From {@link layerRootFor}.
|
|
162
|
+
* @returns {Promise<string[]>}
|
|
163
|
+
*/
|
|
164
|
+
export async function surfacesIn(layerRoot) {
|
|
165
|
+
const entries = await readdir(join(layerRoot, 'surfaces'), { withFileTypes: true }).catch(() => []);
|
|
166
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
104
169
|
/**
|
|
105
170
|
* Where `nina upgrade --apply` writes down a move while it is in flight: the versions it moves between, the
|
|
106
171
|
* files it composes for the first time, and — under {@link movedFrom} — every file it can touch, as it was.
|
package/src/shell.mjs
CHANGED
|
@@ -13,8 +13,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
import { createInterface } from 'node:readline';
|
|
16
|
-
import {
|
|
16
|
+
import { printBanner } from './banner.mjs';
|
|
17
17
|
import { shellHistoryFile } from './paths.mjs';
|
|
18
|
+
import { dim, pink } from './look.mjs';
|
|
18
19
|
|
|
19
20
|
/** Words the session answers itself rather than handing to a command. */
|
|
20
21
|
const LEAVE = new Set(['exit', 'quit', 'q']);
|
|
@@ -87,8 +88,7 @@ function keep(line) {
|
|
|
87
88
|
*/
|
|
88
89
|
export function shell({ bin, version, commands }) {
|
|
89
90
|
const terminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
90
|
-
const
|
|
91
|
-
const prompt = useColor() ? `\x1b[38;2;${PINK.join(';')}mnina ›\x1b[0m ` : 'nina › ';
|
|
91
|
+
const prompt = `${pink('nina ›')} `;
|
|
92
92
|
|
|
93
93
|
printBanner(version);
|
|
94
94
|
console.log(dim(' type a command — stats, check, upgrade --to <version> — `help` lists them, `clear` starts the screen over, `exit` leaves\n'));
|
package/src/steps.mjs
CHANGED
|
@@ -6,16 +6,13 @@
|
|
|
6
6
|
* project forward was six commands in a fixed order, where getting the order wrong quietly
|
|
7
7
|
* overwrote the project's own file — so the order belongs in the tool, not in a message.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* so a log, a CI run and a pipe
|
|
9
|
+
* On a terminal the step running shows as `...` until its line replaces it. Not animated: a step runs
|
|
10
|
+
* synchronously, so a timer would never fire while it works. A log, a CI run and a pipe read like the
|
|
11
|
+
* terminal does, minus that line.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
/** Frames of the ellipsis, slow enough to read and fast enough to look alive. */
|
|
14
|
-
const FRAMES = ['. ', '.. ', '...'];
|
|
15
|
-
const TICK = 220;
|
|
16
|
-
|
|
17
14
|
/**
|
|
18
|
-
* Whether to
|
|
15
|
+
* Whether to show the step running — a pipe, a CI log and a test all get plain lines instead.
|
|
19
16
|
*
|
|
20
17
|
* @param {NodeJS.WriteStream} stream
|
|
21
18
|
* @returns {boolean}
|
|
@@ -46,24 +43,13 @@ export function runSteps(steps, options = {}) {
|
|
|
46
43
|
continue;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
let timer = null;
|
|
51
|
-
if (live) {
|
|
52
|
-
stream.write(` ${FRAMES[0]} ${step.label}`);
|
|
53
|
-
timer = setInterval(() => {
|
|
54
|
-
frame = (frame + 1) % FRAMES.length;
|
|
55
|
-
stream.write(`\r ${FRAMES[frame]} ${step.label}`);
|
|
56
|
-
}, TICK);
|
|
57
|
-
timer.unref?.();
|
|
58
|
-
}
|
|
59
|
-
|
|
46
|
+
if (live) stream.write(` ... ${step.label}`);
|
|
60
47
|
let result;
|
|
61
48
|
try {
|
|
62
49
|
result = step.run();
|
|
63
50
|
} catch (error) {
|
|
64
51
|
result = { ok: false, log: String(error?.stack ?? error) };
|
|
65
52
|
} finally {
|
|
66
|
-
if (timer) clearInterval(timer);
|
|
67
53
|
// `\r` alone leaves the tail of the longest line behind when the next one is shorter.
|
|
68
54
|
if (live) stream.write('\r\u001b[K');
|
|
69
55
|
}
|
package/src/store.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const UNREAD = new Set(['UNCLEAR', 'NONE', null, undefined]);
|
|
|
21
21
|
* @param {string} text - The file.
|
|
22
22
|
* @returns {object[]}
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
function parseStore(text) {
|
|
25
25
|
return String(text)
|
|
26
26
|
.split('\n')
|
|
27
27
|
.filter((l) => l.trim())
|
|
@@ -74,7 +74,7 @@ export function readStoreFile(file, { raw = false } = {}) {
|
|
|
74
74
|
* @param {string} dir - The project.
|
|
75
75
|
* @returns {string[]}
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
function storeNamesOf(dir) {
|
|
78
78
|
const names = new Set([slugFor(dir)]);
|
|
79
79
|
try {
|
|
80
80
|
names.add(slugFor(realpathSync(dir)));
|
package/src/wiring.mjs
CHANGED
|
@@ -22,11 +22,11 @@ import { join } from 'node:path';
|
|
|
22
22
|
/** The composed scripts the hooks run. */
|
|
23
23
|
export const CHECK = 'scripts/harness-check.mjs';
|
|
24
24
|
export const GATE = 'scripts/loop-gate.mjs';
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const GUARD = 'scripts/edit-guard.mjs';
|
|
26
|
+
const COST = 'scripts/cost-watch.mjs';
|
|
27
27
|
|
|
28
28
|
/** The npm scripts the detectors hang off: the reviewer runs one, the other is a detector itself. */
|
|
29
|
-
|
|
29
|
+
const SCRIPTS = {
|
|
30
30
|
'harness:check': 'node scripts/harness-check.mjs',
|
|
31
31
|
'harness:compose:check': 'nina compose --check --quiet',
|
|
32
32
|
};
|
|
@@ -73,7 +73,7 @@ export function packageInstalled(target) {
|
|
|
73
73
|
* @param {string} module - `gate`, `guard`, …
|
|
74
74
|
* @returns {boolean}
|
|
75
75
|
*/
|
|
76
|
-
|
|
76
|
+
function packageExports(target, module) {
|
|
77
77
|
try {
|
|
78
78
|
createRequire(join(target, 'package.json')).resolve(`@xhulz/nina/${module}`);
|
|
79
79
|
return true;
|
|
@@ -111,7 +111,7 @@ const previousCommands = (h) => [command(h.script, h.mode), ...EARLIER_CANNOT_ST
|
|
|
111
111
|
* loop gate has four, one per fact it keeps: a verdict, a dispatch, the owner speaking, and the
|
|
112
112
|
* decision itself; and a fifth, as each stage starts, that hands the stage its lessons (`src/lessons.mjs`).
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
const HOOKS = [
|
|
115
115
|
{
|
|
116
116
|
event: 'Stop', script: CHECK, mode: '--hook', timeout: 30, statusMessage: 'Checking the harness...', why: 'nobody is told what a turn left behind',
|
|
117
117
|
crash: (cannot) => ({ systemMessage: `NINA: the harness check could not start — ${cannot}` }),
|
|
@@ -133,8 +133,10 @@ export const HOOKS = [
|
|
|
133
133
|
{ event: 'PostToolUse', matcher: 'Agent|Task|SendMessage|AskUserQuestion|SubagentHandback', script: GATE, timeout: 10, why: 'the gate never learns which rounds went out, or that the owner answered' },
|
|
134
134
|
{ event: 'SubagentStop', script: GATE, timeout: 10, why: 'the gate misses the verdict of a stage that wrote its report as its last message' },
|
|
135
135
|
{ event: 'SubagentStart', script: GATE, timeout: 10, why: 'a stage is not handed its lessons, and opens every shared pill to find them, or none' },
|
|
136
|
+
{ event: 'SessionStart', matcher: 'compact', script: GATE, timeout: 10, why: 'after a compaction the orchestrator has lost the graph, the router and its open loops, and nothing gives them back' },
|
|
136
137
|
{
|
|
137
|
-
event: 'PreToolUse', matcher: 'Edit|Write|MultiEdit|NotebookEdit', script: GUARD, timeout: 10,
|
|
138
|
+
event: 'PreToolUse', matcher: 'Edit|Write|MultiEdit|NotebookEdit|Bash', script: GUARD, timeout: 10,
|
|
139
|
+
why: "a hand edit to a composed file is found only after the turn, as drift, and a stage's git stash or checkout takes the work in flight",
|
|
138
140
|
crash: (cannot) => ({ systemMessage: `NINA: the edit guard could not start, so composed files can be edited in place — ${cannot}` }),
|
|
139
141
|
},
|
|
140
142
|
// Every tool, since what it watches is a subagent's every turn; a call outside a subagent returns at once.
|
|
@@ -306,6 +308,17 @@ function settingsHooks(hooks) {
|
|
|
306
308
|
return out;
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
/** How {@link missingWiring} opens the sentence that says the package is missing. */
|
|
312
|
+
const NOT_INSTALLED = '@xhulz/nina is not installed in this project';
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Whether what {@link missingWiring} found includes the package itself: the one thing `wire` cannot do.
|
|
316
|
+
*
|
|
317
|
+
* @param {string[]} unwired - Its sentences.
|
|
318
|
+
* @returns {boolean}
|
|
319
|
+
*/
|
|
320
|
+
export const lacksPackage = (unwired) => unwired.some((w) => w.startsWith(NOT_INSTALLED));
|
|
321
|
+
|
|
309
322
|
/**
|
|
310
323
|
* Whatever of the wiring a project lacks, one sentence each.
|
|
311
324
|
*
|
|
@@ -318,7 +331,7 @@ export async function missingWiring(target, shipped) {
|
|
|
318
331
|
// First, because nothing below matters without it: the composed scripts import the package.
|
|
319
332
|
if ((shipped.has(CHECK) || shipped.has(GATE) || shipped.has(GUARD)) && !packageInstalled(target)) {
|
|
320
333
|
out.push(
|
|
321
|
-
|
|
334
|
+
`${NOT_INSTALLED}, so the composed scripts cannot load and every hook fails — ` +
|
|
322
335
|
'pnpm add -D -E @xhulz/nina',
|
|
323
336
|
);
|
|
324
337
|
} else {
|