@xhulz/nina 0.33.0 → 0.35.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/README.md +4 -2
- package/bin/nina.mjs +219 -48
- package/package.json +1 -1
- package/releases/0.34.0/core/GAPS.md +125 -0
- package/releases/0.34.0/core/answered.json +29 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
- package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.34.0/core/tree/.claude/router.md +198 -0
- package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.34.0/core/vocabulary.json +13 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/core/GAPS.md +125 -0
- package/releases/0.35.0/core/answered.json +29 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.35.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.35.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.35.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.35.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.35.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.35.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.35.0/core/tree/.claude/agents/qa.md +153 -0
- package/releases/0.35.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.35.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.35.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.35.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.35.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.35.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.35.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.35.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.35.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.35.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.35.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.35.0/core/tree/.claude/router.md +198 -0
- package/releases/0.35.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.35.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.35.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.35.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.35.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.35.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.35.0/core/vocabulary.json +13 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.35.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.35.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.35.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.35.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.35.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.35.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/agentrun.mjs +91 -12
- package/src/commands/check.mjs +12 -8
- package/src/commands/compose.mjs +85 -9
- package/src/commands/export.mjs +9 -7
- package/src/commands/init.mjs +13 -4
- package/src/commands/langfuse.mjs +25 -10
- package/src/commands/learn.mjs +12 -3
- package/src/commands/release.mjs +21 -3
- package/src/commands/stats.mjs +64 -12
- package/src/commands/upgrade.mjs +131 -25
- package/src/guard.mjs +7 -37
- package/src/langfuse.mjs +31 -11
- package/src/paths.mjs +50 -1
- package/src/prices.mjs +7 -7
- package/src/transcripts.mjs +27 -1
- package/src/vocabulary.mjs +12 -1
- package/src/wiring.mjs +106 -41
package/src/commands/stats.mjs
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
|
|
15
15
|
import { readFile, readdir } from 'node:fs/promises';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import { createReadStream, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
17
|
+
import { closeSync, createReadStream, existsSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { basename, join } from 'node:path';
|
|
21
|
-
import { ROLE_TOKENS, isLoopBack, runOf } from '../transcripts.mjs';
|
|
21
|
+
import { ROLE_TOKENS, isLoopBack, runOf, transcriptsOf } from '../transcripts.mjs';
|
|
22
22
|
import { frontmatter, pillFiles } from './pills.mjs';
|
|
23
|
-
import { HARNESS, snapshotsDir } from '../paths.mjs';
|
|
23
|
+
import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
|
|
24
24
|
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
25
25
|
import { layerRootFor } from './compose.mjs';
|
|
26
26
|
import { PRICES_AS_OF, costOf } from '../prices.mjs';
|
|
@@ -80,17 +80,64 @@ async function load(dir, opts) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
/** Directories already found for an encoded name: `stats` asks for the same project many times over. */
|
|
84
|
+
const decoded = new Map();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The directory a project's sessions ran in, as its own transcripts record it: every line carries the
|
|
88
|
+
* session's `cwd`. Taken only when it encodes back to the same name and is still there.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} encoded - A snapshot's project name, which is also its transcript directory's.
|
|
91
|
+
* @returns {string|null}
|
|
92
|
+
*/
|
|
93
|
+
function recordedDir(encoded) {
|
|
94
|
+
const dir = transcriptsOf(encoded);
|
|
95
|
+
let files = [];
|
|
96
|
+
try {
|
|
97
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
for (const file of files.slice(0, 5)) {
|
|
102
|
+
let head = '';
|
|
103
|
+
try {
|
|
104
|
+
const fd = openSync(join(dir, file), 'r');
|
|
105
|
+
try {
|
|
106
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
107
|
+
head = buffer.toString('utf8', 0, readSync(fd, buffer, 0, buffer.length, 0));
|
|
108
|
+
} finally {
|
|
109
|
+
closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const line of head.split('\n')) {
|
|
115
|
+
let cwd;
|
|
116
|
+
try {
|
|
117
|
+
cwd = JSON.parse(line)?.cwd;
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (typeof cwd === 'string' && slugFor(cwd) === encoded && existsSync(cwd)) return cwd;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
83
127
|
/**
|
|
84
128
|
* Turns a snapshot's encoded project name back into a directory.
|
|
85
129
|
*
|
|
86
|
-
* The encoding
|
|
87
|
-
*
|
|
88
|
-
*
|
|
130
|
+
* The encoding makes every character that is not a letter or a digit a dash, so the name alone cannot say
|
|
131
|
+
* what it was. The project's transcripts can: each line records the directory its session ran in. Failing
|
|
132
|
+
* that, the split is resolved against the file system — which can only rebuild `/` from a dash, so a
|
|
133
|
+
* project whose path held a `.`, a space or an `_` was never found, and its pills, models and requests went
|
|
134
|
+
* missing from every report without a word.
|
|
89
135
|
*
|
|
90
136
|
* @param {string} encoded - The `project` field of a snapshot record.
|
|
91
137
|
* @returns {string | null} The directory, or `null` when no split of the name exists.
|
|
92
138
|
*/
|
|
93
139
|
export function decodeProjectDir(encoded) {
|
|
140
|
+
if (decoded.has(encoded)) return decoded.get(encoded);
|
|
94
141
|
const parts = encoded.replace(/^-/, '').split('-');
|
|
95
142
|
const walk = (base, i) => {
|
|
96
143
|
if (i === parts.length) return base;
|
|
@@ -102,7 +149,10 @@ export function decodeProjectDir(encoded) {
|
|
|
102
149
|
}
|
|
103
150
|
return null;
|
|
104
151
|
};
|
|
105
|
-
|
|
152
|
+
const found = recordedDir(encoded) ?? walk('/', 0);
|
|
153
|
+
// Only what was found: a directory made after a miss is found the next time it is asked for.
|
|
154
|
+
if (found) decoded.set(encoded, found);
|
|
155
|
+
return found;
|
|
106
156
|
}
|
|
107
157
|
|
|
108
158
|
/**
|
|
@@ -224,8 +274,8 @@ function costReport(records) {
|
|
|
224
274
|
}
|
|
225
275
|
|
|
226
276
|
/**
|
|
227
|
-
* How each stage's rounds spent their context: turns, tool calls sent per turn,
|
|
228
|
-
* re-read when the round began and at its largest. Every turn is billed the whole context again, so the
|
|
277
|
+
* How each stage's rounds spent their context: turns, tool calls sent per turn, the context a turn
|
|
278
|
+
* re-read when the round began and at its largest, and how long the prompt that opened the round was. Every turn is billed the whole context again, so the
|
|
229
279
|
* cost of a round is its turns times its context, and a round that starts where a long one ended pays for
|
|
230
280
|
* all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
|
|
231
281
|
*
|
|
@@ -236,8 +286,9 @@ function contextReport(records) {
|
|
|
236
286
|
if (shaped.length === 0) return;
|
|
237
287
|
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
|
|
238
288
|
const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
|
|
239
|
-
|
|
240
|
-
console.log(
|
|
289
|
+
const chars = (xs) => (xs.length ? median(xs).toLocaleString('en-US') : '—');
|
|
290
|
+
console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended; the prompt it was handed is in characters'));
|
|
291
|
+
console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}${'prompt'.padStart(9)}`));
|
|
241
292
|
const byRole = new Map();
|
|
242
293
|
for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
|
|
243
294
|
for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
|
|
@@ -251,7 +302,8 @@ function contextReport(records) {
|
|
|
251
302
|
const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
|
|
252
303
|
console.log(
|
|
253
304
|
` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
|
|
254
|
-
`${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}
|
|
305
|
+
`${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}` +
|
|
306
|
+
`${chars(group.map((r) => r.prompt_chars).filter((n) => typeof n === 'number')).padStart(9)}`,
|
|
255
307
|
);
|
|
256
308
|
});
|
|
257
309
|
}
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
* already wrote that the new harness has nowhere to put. A slot that was renamed or dropped
|
|
7
7
|
* takes the project's own text out of the composition with it, silently, because a fragment
|
|
8
8
|
* nothing references composes to nothing and no check notices. So this reports first and
|
|
9
|
-
* writes only when told to, and
|
|
10
|
-
* fragments is work, and work does not get done by a flag.
|
|
9
|
+
* writes only when told to. Reconciling the fragments is work, and work does not get done by a flag.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import { readFile, readdir
|
|
12
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
14
13
|
import { spawnSync } from 'node:child_process';
|
|
15
|
-
import { HARNESS, legacyHint } from '../paths.mjs';
|
|
16
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { HARNESS, describeMove, interruptedMove, legacyHint, moveJournal, movedFrom } from '../paths.mjs';
|
|
15
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
16
|
import { dirname, join, resolve } from 'node:path';
|
|
18
|
-
import { composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
|
|
17
|
+
import { REMOVED, composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
|
|
19
18
|
import { NEEDS, detected } from '../surfaces.mjs';
|
|
20
19
|
import { filledSlots, projectSlots, referencedVocabulary } from './check.mjs';
|
|
21
20
|
import { EXPECT_ENV } from '../expected.mjs';
|
|
@@ -25,6 +24,9 @@ import { missingFragment, missingWiring, shippedScripts } from '../wiring.mjs';
|
|
|
25
24
|
import { runSteps } from '../steps.mjs';
|
|
26
25
|
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
27
26
|
|
|
27
|
+
/** What stops a move from outside it: Ctrl-C, a `kill`, the terminal closing. */
|
|
28
|
+
const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
29
|
+
|
|
28
30
|
/** Banner lines, which are for a person watching and only noise inside a captured log. */
|
|
29
31
|
const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
|
|
30
32
|
|
|
@@ -75,7 +77,7 @@ function nina(ctx, target, args) {
|
|
|
75
77
|
cwd: target,
|
|
76
78
|
encoding: 'utf8',
|
|
77
79
|
});
|
|
78
|
-
const out = [result.stdout ?? '', result.stderr ?? '']
|
|
80
|
+
const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : '']
|
|
79
81
|
.join('\n')
|
|
80
82
|
.split('\n')
|
|
81
83
|
.filter((line) => line.trim() && !BANNER.test(line))
|
|
@@ -121,7 +123,7 @@ function shell(target, script) {
|
|
|
121
123
|
encoding: 'utf8',
|
|
122
124
|
env: { ...process.env, PATH: `${join(target, 'node_modules', '.bin')}:${process.env.PATH ?? ''}` },
|
|
123
125
|
});
|
|
124
|
-
const out = [result.stdout ?? '', result.stderr ?? ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
|
|
126
|
+
const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
|
|
125
127
|
return { ok: result.status === 0, out, summary: summarise(out) };
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -157,6 +159,74 @@ function summarise(out) {
|
|
|
157
159
|
return [...lines].reverse().find((l) => /^\w[\w ]*:/.test(l))?.trim();
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Puts a project back on the version a move started from: the pin, then the files the move composed for the
|
|
164
|
+
* first time removed, a recompose, and every file it could touch written back as it was — the recompose alone
|
|
165
|
+
* would write the old composition over the owner's own edits and call it restored.
|
|
166
|
+
*
|
|
167
|
+
* @param {{root: string}} ctx - CLI context.
|
|
168
|
+
* @param {string} target - The project.
|
|
169
|
+
* @param {string} profilePath - Its profile.
|
|
170
|
+
* @param {object} profile - The profile as read, whose pin is set back.
|
|
171
|
+
* @param {string} version - The version the move started from.
|
|
172
|
+
* @param {string[]} created - Paths the move composed where nothing was.
|
|
173
|
+
* @param {Map<string, Buffer>} saved - Every file the move could touch, as it was.
|
|
174
|
+
* @returns {boolean} Whether the recompose passed.
|
|
175
|
+
*/
|
|
176
|
+
function restore(ctx, target, profilePath, profile, version, created, saved) {
|
|
177
|
+
profile.core = version;
|
|
178
|
+
writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
|
|
179
|
+
for (const p of created) rmSync(join(target, p), { force: true });
|
|
180
|
+
const recomposed = nina(ctx, target, ['compose', '--project', target]);
|
|
181
|
+
for (const [p, bytes] of saved) {
|
|
182
|
+
mkdirSync(dirname(join(target, p)), { recursive: true });
|
|
183
|
+
writeFileSync(join(target, p), bytes);
|
|
184
|
+
}
|
|
185
|
+
return recomposed.ok;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `nina upgrade --abort`: undoes a move that was stopped before it finished, from what it wrote down first.
|
|
190
|
+
*
|
|
191
|
+
* @param {{root: string}} ctx - CLI context.
|
|
192
|
+
* @param {string} target - The project.
|
|
193
|
+
* @param {string} profilePath - Its profile.
|
|
194
|
+
* @param {object} profile - The profile as read.
|
|
195
|
+
* @param {ReturnType<typeof interruptedMove>} stopped - The move it was left in.
|
|
196
|
+
* @returns {number} Process exit code.
|
|
197
|
+
*/
|
|
198
|
+
function abort(ctx, target, profilePath, profile, stopped) {
|
|
199
|
+
if (!stopped) {
|
|
200
|
+
console.log(' no upgrade was stopped half-way here — there is nothing to undo.\n');
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
if (stopped.unreadable) {
|
|
204
|
+
console.error(
|
|
205
|
+
` ${describeMove(stopped)} Nothing can be undone from it: set "core" in ${HARNESS}/profile.json back by hand, run \`nina compose\`, ` +
|
|
206
|
+
`and delete ${HARNESS}/upgrade.json.\n`,
|
|
207
|
+
);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const saved = new Map();
|
|
211
|
+
for (const p of stopped.saved ?? []) {
|
|
212
|
+
const copy = join(movedFrom(target), p);
|
|
213
|
+
if (existsSync(copy)) saved.set(p, readFileSync(copy));
|
|
214
|
+
}
|
|
215
|
+
const lost = (stopped.saved ?? []).filter((p) => !saved.has(p));
|
|
216
|
+
if (lost.length > 0) {
|
|
217
|
+
console.error(` ${lost.length} of the files the move kept are missing from ${HARNESS}/upgrade-saved/, so it cannot be undone exactly: ${lost.slice(0, 5).join(', ')}${lost.length > 5 ? ', …' : ''}\n`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
const recomposed = restore(ctx, target, profilePath, profile, stopped.from, stopped.created ?? [], saved);
|
|
221
|
+
rmSync(moveJournal(target), { force: true });
|
|
222
|
+
rmSync(movedFrom(target), { recursive: true, force: true });
|
|
223
|
+
console.log(
|
|
224
|
+
` undid the move to ${stopped.to}: pinned ${stopped.from} again and put ${saved.size} file(s) back as they were.` +
|
|
225
|
+
`${recomposed ? '' : ' Recomposing failed along the way — run `nina compose` yourself.'} Run the move again when ready.\n`,
|
|
226
|
+
);
|
|
227
|
+
return recomposed ? 0 : 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
160
230
|
/**
|
|
161
231
|
* `nina upgrade`.
|
|
162
232
|
*
|
|
@@ -176,6 +246,15 @@ export async function upgrade(argv, ctx) {
|
|
|
176
246
|
}
|
|
177
247
|
const profile = JSON.parse(await readFile(profilePath, 'utf8'));
|
|
178
248
|
|
|
249
|
+
// A move stopped half-way comes before anything else asked of this command: its pin may already name the
|
|
250
|
+
// version it was moving to, and "already pins" left the tree half-moved for good.
|
|
251
|
+
const stopped = interruptedMove(target);
|
|
252
|
+
if (argv.includes('--abort')) return abort(ctx, target, profilePath, profile, stopped);
|
|
253
|
+
if (stopped) {
|
|
254
|
+
console.error(` ${describeMove(stopped)} \`nina upgrade --abort --project ${target}\` undoes it; then run the move again.\n`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
179
258
|
const to = arg('--to');
|
|
180
259
|
if (!to) {
|
|
181
260
|
console.error(' usage: nina upgrade --to <version> [--project <dir>] [--apply]\n');
|
|
@@ -354,10 +433,6 @@ export async function upgrade(argv, ctx) {
|
|
|
354
433
|
}
|
|
355
434
|
|
|
356
435
|
const previous = profile.core;
|
|
357
|
-
const pin = async (version) => {
|
|
358
|
-
profile.core = version;
|
|
359
|
-
await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
|
|
360
|
-
};
|
|
361
436
|
|
|
362
437
|
// What was already failing is not this upgrade's doing, and rolling back for it would blame
|
|
363
438
|
// the move for a problem the project brought with it. Measured before anything is written,
|
|
@@ -437,8 +512,12 @@ export async function upgrade(argv, ctx) {
|
|
|
437
512
|
const keptEdited = dropped.filter((p) => edited.has(p));
|
|
438
513
|
// Every file the move can touch, as it is now, so a rollback puts the tree back exactly — the
|
|
439
514
|
// recompose alone would write the old composition over the owner's own edits and call it restored.
|
|
515
|
+
// That is every path either version composes for any surface, not only the declared ones: compose moves
|
|
516
|
+
// a file gated on an undeclared surface to .nina/removed/, and one left out of here stayed there after a
|
|
517
|
+
// rollback that said the tree was as it was.
|
|
518
|
+
const reachable = [...(await composedPaths(from.dir, offeredBefore)), ...(await composedPaths(onto.dir, available))];
|
|
440
519
|
const saved = new Map();
|
|
441
|
-
for (const p of new Set([...pathsBefore, ...pathsAfter])) {
|
|
520
|
+
for (const p of new Set([...pathsBefore, ...pathsAfter, ...reachable])) {
|
|
442
521
|
const entry = entryAt(p);
|
|
443
522
|
if (entry?.isFile()) saved.set(p, readFileSync(join(target, p)));
|
|
444
523
|
}
|
|
@@ -449,7 +528,31 @@ export async function upgrade(argv, ctx) {
|
|
|
449
528
|
writeFileSync(copy, saved.get(p) ?? readFileSync(join(target, p)));
|
|
450
529
|
}
|
|
451
530
|
|
|
452
|
-
|
|
531
|
+
// All of it written down before the pin moves, copies first and the journal last, so a journal always has
|
|
532
|
+
// its copies. A move stopped from here on — Ctrl-C, a closed terminal, a crash — left the pin moved, the tree
|
|
533
|
+
// half-composed, and the owner's own edits, held only in this process's memory, gone; now the next run says
|
|
534
|
+
// so, and `--abort` undoes it.
|
|
535
|
+
rmSync(movedFrom(target), { recursive: true, force: true });
|
|
536
|
+
for (const [p, bytes] of saved) {
|
|
537
|
+
mkdirSync(dirname(join(movedFrom(target), p)), { recursive: true });
|
|
538
|
+
writeFileSync(join(movedFrom(target), p), bytes);
|
|
539
|
+
}
|
|
540
|
+
const journal = { from: previous, to, started: new Date().toISOString(), created, saved: [...saved.keys()] };
|
|
541
|
+
writeFileSync(`${moveJournal(target)}.tmp`, `${JSON.stringify(journal, null, 2)}\n`);
|
|
542
|
+
renameSync(`${moveJournal(target)}.tmp`, moveJournal(target));
|
|
543
|
+
// Ctrl-C reaches the step running in a child too, which dies of it, and its step fails: with this process
|
|
544
|
+
// still alive, that is an ordinary failed step, and the move rolls back. Without a listener this process died
|
|
545
|
+
// first, half-way through, with nothing left to roll anything back.
|
|
546
|
+
const hold = () => {};
|
|
547
|
+
for (const s of SIGNALS) process.on(s, hold);
|
|
548
|
+
const settle = () => {
|
|
549
|
+
for (const s of SIGNALS) process.off(s, hold);
|
|
550
|
+
rmSync(moveJournal(target), { force: true });
|
|
551
|
+
rmSync(movedFrom(target), { recursive: true, force: true });
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
profile.core = to;
|
|
555
|
+
writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
|
|
453
556
|
console.log(` pinned ${to}\n`);
|
|
454
557
|
|
|
455
558
|
const steps = [
|
|
@@ -473,12 +576,20 @@ export async function upgrade(argv, ctx) {
|
|
|
473
576
|
const { failed } = runSteps(steps);
|
|
474
577
|
|
|
475
578
|
if (!failed) {
|
|
579
|
+
settle();
|
|
476
580
|
delete process.env.NINA_UPGRADE;
|
|
477
581
|
console.log(`\n upgrade: ${previous} → ${to} applied and verified.`);
|
|
478
582
|
if (removable.length > 0) console.log(`\n removed ${removable.length} file(s) ${to} no longer composes: ${removable.join(', ')}`);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
583
|
+
// A file gated on a surface the project does not declare is moved aside by compose itself, edited or not.
|
|
584
|
+
const stillKept = keptEdited.filter((p) => entryAt(p));
|
|
585
|
+
const setAside = keptEdited.filter((p) => !entryAt(p));
|
|
586
|
+
if (stillKept.length > 0) {
|
|
587
|
+
console.log(`\n kept ${stillKept.length} file(s) ${to} no longer composes, because they carry edits of yours — delete them once nothing in them is needed:`);
|
|
588
|
+
for (const p of stillKept) console.log(` ${p}`);
|
|
589
|
+
}
|
|
590
|
+
if (setAside.length > 0) {
|
|
591
|
+
console.log(`\n moved ${setAside.length} file(s) with edits of yours to ${REMOVED}/, since ${to} composes them only for a surface this project does not declare:`);
|
|
592
|
+
for (const p of setAside) console.log(` ${p}`);
|
|
482
593
|
}
|
|
483
594
|
if (occupied.length > 0) {
|
|
484
595
|
console.log(`\n replaced ${occupied.length} file(s) of yours with what ${to} composes there — the originals are in ${HARNESS}/replaced/:`);
|
|
@@ -530,15 +641,10 @@ export async function upgrade(argv, ctx) {
|
|
|
530
641
|
}
|
|
531
642
|
|
|
532
643
|
console.log(`\n ✗ ${failed.label} failed — rolling back to ${previous}.\n`);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const recomposed = nina(ctx, target, ['compose', '--project', target]);
|
|
536
|
-
for (const [p, bytes] of saved) {
|
|
537
|
-
mkdirSync(dirname(join(target, p)), { recursive: true });
|
|
538
|
-
writeFileSync(join(target, p), bytes);
|
|
539
|
-
}
|
|
644
|
+
const recomposed = restore(ctx, target, profilePath, profile, previous, created, saved);
|
|
645
|
+
settle();
|
|
540
646
|
console.log(
|
|
541
|
-
recomposed
|
|
647
|
+
recomposed
|
|
542
648
|
? ` rolled back: pinned ${previous} and recomposed. The tree is as it was.\n`
|
|
543
649
|
: ` rolled back the pin to ${previous}, but recomposing failed — run \`nina compose\` yourself.\n`,
|
|
544
650
|
);
|
package/src/guard.mjs
CHANGED
|
@@ -20,21 +20,18 @@
|
|
|
20
20
|
import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from 'node:fs';
|
|
21
21
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { composedPaths } from './commands/compose.mjs';
|
|
23
|
+
import { NOTICE_HEAD, composedPaths, noticeOf } from './commands/compose.mjs';
|
|
24
24
|
import { HARNESS } from './paths.mjs';
|
|
25
25
|
|
|
26
|
+
// Read here and by the suites from here; the test itself lives beside the compiler that writes the notice.
|
|
27
|
+
export { NOTICE_HEAD, noticeOf };
|
|
28
|
+
|
|
26
29
|
/** The package this runs from: its releases say whether a pinned version ships the guard. */
|
|
27
30
|
const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
28
31
|
|
|
29
32
|
/** The tools that change a file, and where each names it. */
|
|
30
33
|
const EDITS = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' };
|
|
31
34
|
|
|
32
|
-
/**
|
|
33
|
-
* How far into a file the notice is looked for: below a frontmatter block, or a shebang. The compose
|
|
34
|
-
* suite holds every composed file's notice inside it.
|
|
35
|
-
*/
|
|
36
|
-
export const NOTICE_HEAD = 60;
|
|
37
|
-
|
|
38
35
|
/** How much of a file is read to find it: the head, not the whole of a large file on every edit. */
|
|
39
36
|
const HEAD_BYTES = 16 * 1024;
|
|
40
37
|
|
|
@@ -69,35 +66,6 @@ function head(path) {
|
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
/**
|
|
73
|
-
* A composed file's notice, as one sentence without its comment syntax, or null when it has none.
|
|
74
|
-
*
|
|
75
|
-
* @param {string} text - The file.
|
|
76
|
-
* @returns {string|null}
|
|
77
|
-
*/
|
|
78
|
-
export function noticeOf(text) {
|
|
79
|
-
const lines = String(text).split('\n').slice(0, NOTICE_HEAD);
|
|
80
|
-
const at = lines.findIndex((l) => /^(<!--|\/\/) nina:generated/.test(l));
|
|
81
|
-
if (at < 0) return null;
|
|
82
|
-
const block = [];
|
|
83
|
-
if (lines[at].startsWith('//')) {
|
|
84
|
-
for (const line of lines.slice(at)) {
|
|
85
|
-
if (!line.startsWith('//')) break;
|
|
86
|
-
block.push(line.replace(/^\/\/ ?/, ''));
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
// The notice's own delimiters, taken off by position: this reads text the compiler wrote, it filters
|
|
90
|
-
// nothing, and a pattern for it read to a scanner as an HTML sanitizer that misses `--!>`.
|
|
91
|
-
for (const line of lines.slice(at)) {
|
|
92
|
-
const open = line.startsWith('<!-- ') ? line.slice('<!-- '.length) : line;
|
|
93
|
-
const end = open.lastIndexOf('-->');
|
|
94
|
-
block.push(end >= 0 ? open.slice(0, end) : open);
|
|
95
|
-
if (end >= 0) break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return block.map((l) => l.trim()).join(' ');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
69
|
/**
|
|
102
70
|
* Handles one hook event: the JSON to print, or null to let the call through. Never throws.
|
|
103
71
|
*
|
|
@@ -121,7 +89,9 @@ export async function handleEdit(input, { root, pkg = PACKAGE }) {
|
|
|
121
89
|
if (!pinned) return null;
|
|
122
90
|
const target = isAbsolute(path) ? path : resolve(input.cwd ?? root, path);
|
|
123
91
|
if (!existsSync(target)) return null;
|
|
124
|
-
|
|
92
|
+
// The native call returns the name as it is on disk: where case is ignored, `claude.md` is CLAUDE.md, and
|
|
93
|
+
// read as spelled it reached no composed path and let the edit through.
|
|
94
|
+
const rel = relative(realpathSync.native(root), realpathSync.native(target)).split(sep).join('/');
|
|
125
95
|
// Outside the project, or in its own layer — where a change to a composed file belongs.
|
|
126
96
|
if (rel.startsWith('..') || isAbsolute(rel) || rel.split('/')[0] === HARNESS) return null;
|
|
127
97
|
const notice = noticeOf(head(target));
|
package/src/langfuse.mjs
CHANGED
|
@@ -100,31 +100,47 @@ function usageAttributes(tokens, model, priced) {
|
|
|
100
100
|
return out;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* How much of a stage's context a project sends, from its switch in the Langfuse configuration: all of it
|
|
105
|
+
* (`--content`), only what passed between the agents (`--prompts`), or none.
|
|
106
|
+
*
|
|
107
|
+
* @param {{content?: boolean|string}|undefined} setting - The project's entry, if it is on.
|
|
108
|
+
* @returns {'full'|'prompts'|null}
|
|
109
|
+
*/
|
|
110
|
+
export function contextMode(setting) {
|
|
111
|
+
return setting?.content === 'prompts' ? 'prompts' : setting?.content ? 'full' : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A round's shape, as metadata on its root: how many turns and calls it took, and how large its context and prompt were. */
|
|
115
|
+
const SHAPE = ['turns', 'tool_calls', 'context_start', 'context_peak', 'prompt_chars'];
|
|
116
|
+
|
|
103
117
|
/**
|
|
104
118
|
* The OTLP span at the root of a dispatch's trace. Without its context, a run that spent tokens is a
|
|
105
119
|
* generation carrying the run's whole usage and estimated cost, and one that did not is a span. With it,
|
|
106
120
|
* the root is the agent — its prompt in, its report out — and the usage is on the messages beneath it,
|
|
107
|
-
* so no sum counts a token twice.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
121
|
+
* so no sum counts a token twice. With the prompt and report alone, nothing is beneath it, so the root is
|
|
122
|
+
* a generation again, carrying the usage beside them. Its verdict is the score beside it rather than an
|
|
123
|
+
* attribute on it: a run whose verdict is read only later can still be given one, since that is a first
|
|
124
|
+
* score and not a second span.
|
|
110
125
|
*
|
|
111
126
|
* @param {object} record - A snapshot record.
|
|
112
127
|
* @param {string} project - The name the project goes by in Langfuse.
|
|
113
128
|
* @param {{prompt: string, report: string}} [context] - What the stage was given and handed back.
|
|
129
|
+
* @param {{usage?: boolean}} [options] - `usage`: the root carries the usage even with its context.
|
|
114
130
|
* @returns {object|null} Null for a record with no time to place it at.
|
|
115
131
|
*/
|
|
116
|
-
export function spanOf(record, project, context = null) {
|
|
132
|
+
export function spanOf(record, project, context = null, { usage = false } = {}) {
|
|
117
133
|
const start = nanos(record.ts);
|
|
118
134
|
if (!start) return null;
|
|
119
135
|
const end = nanos(record.result_ts) ?? start;
|
|
120
|
-
const tokens = !context && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
|
|
136
|
+
const tokens = (!context || usage) && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
|
|
121
137
|
const attributes = [
|
|
122
138
|
attribute('langfuse.session.id', record.session ?? 'no-session'),
|
|
123
139
|
attribute('langfuse.trace.name', record.role),
|
|
124
140
|
attribute('langfuse.trace.tags', ['nina', project]),
|
|
125
141
|
attribute('langfuse.trace.metadata.project', project),
|
|
126
142
|
attribute('langfuse.trace.metadata.role', record.role),
|
|
127
|
-
attribute('langfuse.observation.type',
|
|
143
|
+
attribute('langfuse.observation.type', tokens ? 'generation' : context ? 'agent' : 'span'),
|
|
128
144
|
attribute('langfuse.observation.metadata.role', record.role),
|
|
129
145
|
];
|
|
130
146
|
if (typeof record.issues === 'number') attributes.push(attribute('langfuse.observation.metadata.issues', record.issues));
|
|
@@ -133,6 +149,7 @@ export function spanOf(record, project, context = null) {
|
|
|
133
149
|
if (record.effort) attributes.push(attribute('langfuse.observation.metadata.effort', record.effort));
|
|
134
150
|
// A round after the first is the same agent resumed after it reported; the trace says which.
|
|
135
151
|
if (typeof record.round === 'number') attributes.push(attribute('langfuse.observation.metadata.round', record.round));
|
|
152
|
+
for (const key of SHAPE) if (typeof record[key] === 'number') attributes.push(attribute(`langfuse.observation.metadata.${key}`, record[key]));
|
|
136
153
|
if (tokens) attributes.push(...usageAttributes(tokens, record.usage_model, record.usage_model));
|
|
137
154
|
if (context) {
|
|
138
155
|
attributes.push(attribute('langfuse.observation.input', context.prompt ?? ''));
|
|
@@ -144,23 +161,26 @@ export function spanOf(record, project, context = null) {
|
|
|
144
161
|
/**
|
|
145
162
|
* Everything a dispatch sends: the root, and — when its context goes too — a generation per message it
|
|
146
163
|
* wrote, with that message's own tokens and cost, and an observation per tool it called, with what went
|
|
147
|
-
* in and what came back.
|
|
148
|
-
*
|
|
164
|
+
* in and what came back. With `prompts`, only what passed between the agents goes: the root, with the
|
|
165
|
+
* prompt the stage was given and the report it handed back, and none of what it read, wrote or called in
|
|
166
|
+
* between. Every text is masked (`redact`), cut to a length, and has the owner's home directory written
|
|
167
|
+
* as `~`.
|
|
149
168
|
*
|
|
150
169
|
* @param {object} record - A snapshot record.
|
|
151
170
|
* @param {string} project - The name the project goes by in Langfuse.
|
|
152
171
|
* @param {object|null} run - From `readRun`, or null to send the record alone.
|
|
153
172
|
* @param {{text?: number, tool?: number}} [limits] - How much of each text goes.
|
|
173
|
+
* @param {{prompts?: boolean}} [options] - `prompts`: the prompt and the report, and nothing beneath them.
|
|
154
174
|
* @returns {object[]} Spans; empty for a record with no time to place it at.
|
|
155
175
|
*/
|
|
156
|
-
export function spansOf(record, project, run, limits = {}) {
|
|
176
|
+
export function spansOf(record, project, run, limits = {}, { prompts = false } = {}) {
|
|
157
177
|
const textChars = limits.text ?? TEXT_CHARS;
|
|
158
178
|
const toolChars = limits.tool ?? TOOL_CHARS;
|
|
159
179
|
const home = homedir();
|
|
160
180
|
const clean = (text, chars) => clip(redact(String(text ?? '')).replaceAll(home, '~'), chars);
|
|
161
|
-
const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null);
|
|
181
|
+
const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null, { usage: prompts });
|
|
162
182
|
if (!root) return [];
|
|
163
|
-
if (!run) return [root];
|
|
183
|
+
if (!run || prompts) return [root];
|
|
164
184
|
const child = (id, name, start, end, attributes) => ({
|
|
165
185
|
traceId: root.traceId,
|
|
166
186
|
spanId: hexId(`${record.dispatch_id}|${id}`, 16),
|
package/src/paths.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* project, and losing it is irreversible — so it lives in the user's own directory.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { join, resolve } from 'node:path';
|
|
14
14
|
|
|
@@ -100,3 +100,52 @@ export function legacyHint(target) {
|
|
|
100
100
|
? ` — this project still has .harness/, which is what ${HARNESS}/ used to be called. Rename it; do not start over.`
|
|
101
101
|
: '';
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Where `nina upgrade --apply` writes down a move while it is in flight: the versions it moves between, the
|
|
106
|
+
* files it composes for the first time, and — under {@link movedFrom} — every file it can touch, as it was.
|
|
107
|
+
* It is written before the pin moves and removed once the move is verified or rolled back, so one that is
|
|
108
|
+
* still there is a move that was stopped half-way.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} target - The project.
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export const moveJournal = (target) => join(target, HARNESS, 'upgrade.json');
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Where an in-flight move keeps the files it can touch, as they were before it — at their own paths below.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} target - The project.
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export const movedFrom = (target) => join(target, HARNESS, 'upgrade-saved');
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The move a project was left in when `upgrade --apply` was stopped before it finished, or null when none is.
|
|
125
|
+
*
|
|
126
|
+
* A pin that moved with the composed files half-written looks like hand edits to every check, and the
|
|
127
|
+
* owner's own edits it was about to put back lived only in the stopped process's memory. This is what says
|
|
128
|
+
* so instead, and what `nina upgrade --abort` undoes it from.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} target - The project.
|
|
131
|
+
* @returns {{from: string, to: string, started: string, created: string[], saved: string[]}|{unreadable: string}|null}
|
|
132
|
+
*/
|
|
133
|
+
export function interruptedMove(target) {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(readFileSync(moveJournal(target), 'utf8'));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return error.code === 'ENOENT' ? null : { unreadable: error.message };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A stopped move, said once for every command that finds one.
|
|
143
|
+
*
|
|
144
|
+
* @param {NonNullable<ReturnType<typeof interruptedMove>>} stopped - The move.
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
export function describeMove(stopped) {
|
|
148
|
+
return stopped.unreadable
|
|
149
|
+
? `An upgrade was stopped before it finished, and its journal ${HARNESS}/upgrade.json cannot be read (${stopped.unreadable}).`
|
|
150
|
+
: `An upgrade from ${stopped.from} to ${stopped.to}, started ${stopped.started}, was stopped before it finished — the pin and the composed files may be half-moved.`;
|
|
151
|
+
}
|
package/src/prices.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
/** The date the table was read, printed beside every estimate made from it. */
|
|
16
16
|
export const PRICES_AS_OF = '2026-06-24';
|
|
17
17
|
|
|
18
|
-
/** model id
|
|
18
|
+
/** model id → $/MTok. */
|
|
19
19
|
const PRICES = {
|
|
20
20
|
'claude-fable-5-1': { input: 10, output: 50, read: 0.25 },
|
|
21
21
|
'claude-mythos-5-1': { input: 10, output: 50 },
|
|
@@ -39,12 +39,12 @@ const PRICES = {
|
|
|
39
39
|
* @returns {{input: number, output: number, read: number, write5m: number, write1h: number}|null}
|
|
40
40
|
*/
|
|
41
41
|
export function priceOf(model) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// The id itself, less the two suffixes a transcript adds to one: a context window (`[1m]`) and a snapshot
|
|
43
|
+
// date (`-20251001`). Matched by prefix, a model newer than the table was priced as the one before it —
|
|
44
|
+
// `claude-opus-5-6` as Opus 5, at five times its cache reads.
|
|
45
|
+
const id = String(model ?? '').replace(/\[[^\]]*\]$/, '').replace(/-\d{8}$/, '');
|
|
46
|
+
const p = Object.hasOwn(PRICES, id) ? PRICES[id] : null;
|
|
47
|
+
if (!p) return null;
|
|
48
48
|
return { input: p.input, output: p.output, read: p.read ?? p.input * 0.1, write5m: p.input * 1.25, write1h: p.input * 2 };
|
|
49
49
|
}
|
|
50
50
|
|