@turri-dev/cli 0.2.0 → 0.2.1-next.47b578c

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.
Files changed (80) hide show
  1. package/README.md +3 -2
  2. package/dist/chats.d.ts +31 -4
  3. package/dist/chats.js +146 -52
  4. package/dist/cli.js +4 -2
  5. package/dist/config.d.ts +4 -1
  6. package/dist/config.js +39 -4
  7. package/dist/environment.d.ts +0 -2
  8. package/dist/environment.js +0 -6
  9. package/dist/extension.d.ts +4 -3
  10. package/dist/extension.js +110 -23
  11. package/dist/host-session.d.ts +14 -0
  12. package/dist/host-session.js +2 -0
  13. package/dist/host.d.ts +6 -1
  14. package/dist/host.js +5 -1
  15. package/dist/init-config.d.ts +1 -1
  16. package/dist/init-config.js +33 -3
  17. package/dist/installed-providers.d.ts +40 -0
  18. package/dist/installed-providers.js +98 -0
  19. package/dist/model-check.d.ts +4 -2
  20. package/dist/model-check.js +33 -17
  21. package/dist/notice.d.ts +30 -6
  22. package/dist/pi.js +8 -24
  23. package/dist/plain/files.d.ts +16 -0
  24. package/dist/plain/files.js +68 -5
  25. package/dist/plain/graph.js +2 -2
  26. package/dist/plain/module-concepts.js +3 -2
  27. package/dist/plain/parser.d.ts +8 -3
  28. package/dist/plain/parser.js +47 -6
  29. package/dist/plain/stdlib.js +3 -3
  30. package/dist/regenerate/base-folder.d.ts +2 -0
  31. package/dist/regenerate/base-folder.js +24 -6
  32. package/dist/regenerate/command-check.d.ts +12 -1
  33. package/dist/regenerate/command-check.js +74 -0
  34. package/dist/regenerate/confine.d.ts +5 -0
  35. package/dist/regenerate/confine.js +9 -9
  36. package/dist/regenerate/ledger.d.ts +7 -0
  37. package/dist/regenerate/lineage.js +43 -2
  38. package/dist/regenerate/loop.d.ts +8 -0
  39. package/dist/regenerate/loop.js +11 -4
  40. package/dist/regenerate/os-sandbox.d.ts +2 -0
  41. package/dist/regenerate/os-sandbox.js +55 -20
  42. package/dist/regenerate/release.d.ts +2 -0
  43. package/dist/regenerate/requires.d.ts +12 -0
  44. package/dist/regenerate/requires.js +70 -1
  45. package/dist/regenerate/spec-steps.js +18 -1
  46. package/dist/regenerate/standing.d.ts +28 -0
  47. package/dist/regenerate/standing.js +30 -0
  48. package/dist/regenerate/testing.d.ts +2 -0
  49. package/dist/regenerate/testing.js +14 -0
  50. package/dist/release-agent.d.ts +17 -0
  51. package/dist/release-agent.js +19 -7
  52. package/dist/release-tools.d.ts +6 -0
  53. package/dist/release-tools.js +24 -5
  54. package/dist/sentinel-chat.d.ts +1 -1
  55. package/dist/sentinel-chat.js +21 -8
  56. package/dist/sentinel-check.d.ts +36 -0
  57. package/dist/sentinel-check.js +78 -0
  58. package/dist/sentinel.d.ts +40 -9
  59. package/dist/sentinel.js +240 -37
  60. package/dist/setup.d.ts +24 -3
  61. package/dist/setup.js +117 -31
  62. package/dist/spawn-async.d.ts +18 -0
  63. package/dist/spawn-async.js +35 -0
  64. package/dist/stack.d.ts +14 -0
  65. package/dist/stack.js +44 -2
  66. package/dist/turri-versions.d.ts +71 -0
  67. package/dist/turri-versions.js +275 -0
  68. package/docs/README.md +2 -0
  69. package/docs/claude-subscription.md +131 -0
  70. package/docs/config.md +5 -0
  71. package/docs/deploy.md +2 -2
  72. package/docs/hosting.md +42 -4
  73. package/docs/installation.md +39 -2
  74. package/docs/parser.md +1 -1
  75. package/docs/regenerating.md +1 -1
  76. package/docs/sandbox.md +2 -1
  77. package/docs/sentinel.md +11 -3
  78. package/docs/using.md +4 -2
  79. package/genome/rules/turri-regeneration.md +1 -1
  80. package/package.json +1 -1
package/dist/extension.js CHANGED
@@ -17,6 +17,7 @@ import { sandboxAvailable, sandboxSummary } from "./regenerate/os-sandbox.js";
17
17
  import { findTestScripts, scriptKindOfSkill } from "./regenerate/test-scripts.js";
18
18
  import { writingScript } from "./regenerate/scripts-being-written.js";
19
19
  import { applyBedrockRegion } from "./bedrock-region.js";
20
+ import { bannerWarnings } from "./turri-versions.js";
20
21
  import { conformanceBlock, conformanceTestsOnIn, CONFORMANCE_TOOLS, TURN_ON_TOOL, turnOnConformanceTests, turnOnConformanceTestsTool } from "./conformance-switch.js";
21
22
  import { BACKGROUND_NOTE, doneLine, failedTask, ownChanges, runDetails, skillRunDelivery, startedText } from "./background-skills.js";
22
23
  import { createAskUserTool } from "./tools.js";
@@ -26,7 +27,8 @@ import { describeGates, interruptedRun, runSkill } from "./genome.js";
26
27
  import { unqueue } from "./chat-waiting.js";
27
28
  import { skillStatus } from "./skill.js";
28
29
  import { runningDeployed, stopDeployed } from "./regenerate/release.js";
29
- import { createDeployState, createDeployTools } from "./release-tools.js";
30
+ import { createDeployState, createDeployTools, currentRelease } from "./release-tools.js";
31
+ import { checkedSpecs } from "./sentinel-check.js";
30
32
  import { runReleaseAgent } from "./release-agent.js";
31
33
  import { createSteeringStore, EVERY_MODULE, moduleNameOf, queueSectionOf, steeringFor } from "./regenerate/steer.js";
32
34
  import { askedForCommandLine, refusal, selfInvocation, TOOLS_NOT_CLI } from "./self-invocation.js";
@@ -61,6 +63,8 @@ import { doctorTool } from "./doctor-tool.js";
61
63
  import { ledgerTool } from "./ledger-tools.js";
62
64
  import { describeSpecConflict } from "./regenerate/stop-report.js";
63
65
  import { inheritProviders, inheritThinkingLevel } from "./pi.js";
66
+ import { LOADING_PROVIDERS } from "./installed-providers.js";
67
+ import { machineChange, machineChangeReason, writesPlain } from "./regenerate/command-check.js";
64
68
  import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
65
69
  import { loadRules, renderRulesBlock } from "./rules.js";
66
70
  import { registerTurriConfig, ruleNames } from "./config-command.js";
@@ -74,7 +78,7 @@ import { supersededByTurri } from "./chat-skills.js";
74
78
  import { conversationIdentity } from "./identity.js";
75
79
  import { setSurface, surfaceOf } from "./surface.js";
76
80
  import { createChats, scopedToChat } from "./chats.js";
77
- import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
81
+ import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelCheckBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
78
82
  import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
79
83
  import { currentChat } from "./chat-scope.js";
80
84
  import { registerChats } from "./chat-commands.js";
@@ -404,7 +408,9 @@ export function processSession(make = () => createTurriCore()) {
404
408
  const global = globalThis;
405
409
  return (global[SESSION] ??= make());
406
410
  }
407
- const extension = (pi) => processSession().extension(pi);
411
+ // Loaded as nothing while the person's pi extensions are loaded only for their providers (installed-providers.ts): an older
412
+ // turri installed into every pi is among them, and would start a session of its own.
413
+ const extension = (pi) => (process.env[LOADING_PROVIDERS] === "1" ? undefined : processSession().extension(pi));
408
414
  export default extension;
409
415
  /**
410
416
  * The session's work loop, for a host to read: made with the first chat's context - the models it plans with - when no
@@ -431,8 +437,10 @@ function turriExtension(api, session, link) {
431
437
  const isSentinel = link.role === SENTINEL;
432
438
  const hostSentinelTools = isSentinel ? (host.sentinel?.tools ?? []) : [];
433
439
  const sentinelAllowed = new Set([...SENTINEL_TOOLS, ...hostSentinelTools.map((tool) => tool.name)]);
440
+ // Its resume: the module brought up again on the session's work loop, as regenerate brings it up.
441
+ const resumeModule = async (plainFile) => describeModules(await sessionLoop(session).bringUp(resolve(cwd, plainFile)));
434
442
  if (isSentinel)
435
- registerSentinel(pi, session, cwd, hostSentinelTools, sentinelAllowed);
443
+ registerSentinel(pi, session, cwd, hostSentinelTools, sentinelAllowed, resumeModule);
436
444
  const dialogs = link.dialogs ?? host.dialogs;
437
445
  // turri's own skills, and the project's - .turri/skills, .agents/skills (skills.ts).
438
446
  const discoveredSkills = discoverSkills(PACKAGE_ROOT, cwd);
@@ -491,6 +499,39 @@ function turriExtension(api, session, link) {
491
499
  });
492
500
  void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true });
493
501
  };
502
+ // The sentinel's: functional specs a host asked it to check. The module is released first - as release-after-pulse
503
+ // releases it - unless what runs is its own codebase's last commit; then told, in a conversation of its own for the
504
+ // check, its hidden first message `turri_sentinel_check`. Why not, when the release failed: its agent is told nothing.
505
+ const briefCheck = async (check, context) => {
506
+ const state = deployStates.get(jobs) ?? createDeployState();
507
+ let releasedNow = false;
508
+ if (currentRelease(state, check.plainFile, check.module) === undefined) {
509
+ // Behind, never made, or what it started died: released afresh - the release tool would say it shipped that already.
510
+ state.released.delete(check.module);
511
+ let said;
512
+ try {
513
+ const ctx = contextNow();
514
+ const model = ctx !== undefined ? releaseModel(ctx, check.plainFile) : undefined;
515
+ said = await runReleaseAgent({ plainFile: check.plainFile, cwd, state, inModule: check.module, forCheck: { moduleName: check.module }, ...(model !== undefined ? { model } : {}) });
516
+ }
517
+ catch (error) {
518
+ return error instanceof Error ? error.message : String(error);
519
+ }
520
+ if (currentRelease(state, check.plainFile, check.module) === undefined)
521
+ return said.trim() || "its last commit was not released";
522
+ releasedNow = true;
523
+ }
524
+ const commit = state.released.get(check.module)?.commit;
525
+ const content = sentinelCheckBrief(check, {
526
+ specs: checkedSpecs(check),
527
+ released: runningDeployed(check.module),
528
+ releasedNow,
529
+ ...(commit !== undefined ? { commit } : {}),
530
+ ...(context.focused !== undefined ? { focused: context.focused } : {}),
531
+ });
532
+ void pi.sendMessage({ customType: "turri_sentinel_check", content, display: false, details: { checkId: check.id, module: check.module, funcIds: check.funcIds } }, { triggerTurn: true });
533
+ return undefined;
534
+ };
494
535
  // A pulse ended, with the sentinel on: said in the chat. One that landed is told to its agent too, to tell the person
495
536
  // it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
496
537
  // contradicts itself is put to the person at once, by turri itself, as the chat would have been started on it.
@@ -500,7 +541,9 @@ function turriExtension(api, session, link) {
500
541
  session.notice(specConflictNotice(job, specConflictTask(job.specConflict)));
501
542
  if (options.tell !== true)
502
543
  return;
503
- void pi.sendMessage({ customType: "turri_job_landed", content: landedNote(job, runningDeployed(job.module)), display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true, deliverAs: "followUp" });
544
+ // A host that shows the person the pulse's end itself: the chat knows of it - in order, with no turn of its own.
545
+ const shown = host.pulseEndsShown === true;
546
+ void pi.sendMessage({ customType: "turri_job_landed", content: landedNote(job, runningDeployed(job.module), { shown }), display: false, details: { jobId: job.id, status: job.status } }, shown ? { triggerTurn: false } : { triggerTurn: true, deliverAs: "followUp" });
504
547
  };
505
548
  const notices = noticeDialogs(pi, session, link, contextNow);
506
549
  // The test advisor's verdicts, once the chat is idle: a yes starts a turn that writes its scenarios, a no is a line.
@@ -509,6 +552,7 @@ function turriExtension(api, session, link) {
509
552
  announce,
510
553
  landed,
511
554
  brief,
555
+ briefCheck,
512
556
  notice: notices.put,
513
557
  noticeOver: notices.over,
514
558
  concluded: notices.concluded,
@@ -545,10 +589,9 @@ function turriExtension(api, session, link) {
545
589
  // pi's skills that turri has already - its own and the project's, run with cultivate, and any about ***plain from
546
590
  // another tool - are left out of the list the conversation is offered (chat-skills.ts); the person is told once.
547
591
  const turriRuns = new Set(discoveredSkills.skills.map((skill) => skill.name));
548
- // What the modules need of this machine: worked out once the chat is up, and after a turn when a spec changed - never
549
- // while a message waits to be sent.
592
+ // What the modules need of this machine: worked out when a turn first asks, and after a turn when a spec changed - never
593
+ // as the session starts, which a host would wait on, and never while a message waits to be sent once it is known.
550
594
  const machine = machineNeeds(cwd);
551
- pi.on("session_start", () => machine.refreshSoon());
552
595
  pi.on("agent_settled", () => machine.refreshSoon());
553
596
  pi.on("before_agent_start", (event, ctx) => {
554
597
  const offered = event.systemPromptOptions.skills;
@@ -598,9 +641,20 @@ function turriExtension(api, session, link) {
598
641
  pi.on("tool_call", (event) => {
599
642
  // The sentinel reads, and puts what it finds to the person: whatever else it calls is refused.
600
643
  if (isSentinel && !sentinelAllowed.has(event.toolName)) {
601
- return { block: true, reason: `The sentinel changes nothing: ${event.toolName} is not one of its tools. Put what needs doing to the person with notify_person.` };
644
+ return { block: true, reason: `${event.toolName} is not one of the sentinel's tools: it fixes the project's testing scripts and their config, and nothing else. Put what needs doing to the person with notify_person.` };
602
645
  }
603
646
  const command = event.input.command;
647
+ // turri changes nothing of the machine's - no install, no system setting - asked or not, through any tool that runs a
648
+ // command: its own bash, and a host's - a terminal in its window. The person does that.
649
+ if (typeof command === "string") {
650
+ const changes = machineChange(command);
651
+ if (changes !== undefined)
652
+ return { block: true, reason: machineChangeReason(changes) };
653
+ // The sentinel's write and edit reach its tooling alone: a host's command does not write ***plain for it.
654
+ const plain = isSentinel ? writesPlain(command) : undefined;
655
+ if (plain !== undefined)
656
+ return { block: true, reason: `${plain} is ***plain: the sentinel never changes it - put what it needs to the person with notify_person.` };
657
+ }
604
658
  if (event.toolName === "bash" && typeof command === "string") {
605
659
  const what = selfInvocation(command);
606
660
  if (what !== undefined && !askedForCommandLine(lastUserMessage))
@@ -1183,7 +1237,7 @@ function registerRegenerate(pi, cwd, jobs) {
1183
1237
  // Said at once, and kept in the chat: what follows takes a while - the planner is an agent - and the person sees it
1184
1238
  // began, not a chat that stays empty until the first pulse starts. The plan is said under it once it is made.
1185
1239
  say(scratch
1186
- ? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""}, then planning all of it again…`
1240
+ ? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""} and then starting regeneration…`
1187
1241
  : again.length > 0
1188
1242
  ? `Planning ${again.join(", ")} again…`
1189
1243
  : `Planning ${module}'s pulses…`, "info");
@@ -1444,8 +1498,9 @@ export function machineBlock(cwd, modules = stackOnMachine(cwd)) {
1444
1498
  }
1445
1499
  /**
1446
1500
  * What the project's modules need of this machine, kept between turns - working it out parses every spec, seconds on a
1447
- * project of long requires chains, and a message is never kept waiting for it. Worked out once the chat is up, and again
1448
- * when a turn has settled and a spec changed since: a turn is told what was known last, at most a turn behind.
1501
+ * project of long requires chains. Worked out when the first turn asks - not as the session starts: a host's thread
1502
+ * would be held up opening a chat no one may write in - and again, once known, when a turn has settled and a spec
1503
+ * changed since: a turn is told what was known last, at most a turn behind.
1449
1504
  */
1450
1505
  export function machineNeeds(cwd, work = stackOnMachine) {
1451
1506
  let known;
@@ -1475,8 +1530,10 @@ export function machineNeeds(cwd, work = stackOnMachine) {
1475
1530
  refresh();
1476
1531
  return known.modules;
1477
1532
  },
1478
- /** Worked out again, if a spec changed - once the chat has drawn what it is drawing. */
1533
+ /** Worked out again, if it is known and a spec changed - once the chat has drawn what it is drawing. */
1479
1534
  refreshSoon() {
1535
+ if (known === undefined)
1536
+ return;
1480
1537
  setTimeout(() => {
1481
1538
  try {
1482
1539
  refresh();
@@ -1598,6 +1655,10 @@ function conversationModel(ctx, override) {
1598
1655
  * The chat's model, read now - it can change while a skill runs - or, from a context that has
1599
1656
  * gone stale since, the model it had when it was last readable.
1600
1657
  */
1658
+ /** The model a release runs on - a pulse's last stage, and a check's: the one that regenerated the module. A release follows steps, and a small model thinking little does that well. */
1659
+ function releaseModel(ctx, plainFile) {
1660
+ return modelFor(chosenModels, "spec_changes", projectModel(ctx, plainFile) ?? chatModelOf(ctx));
1661
+ }
1601
1662
  function chatModelOf(ctx) {
1602
1663
  try {
1603
1664
  const now = conversationModel(ctx);
@@ -1833,7 +1894,7 @@ function workLoopFor(jobs, cwd, ctx) {
1833
1894
  // Under release-after-pulse, a pulse releases what it landed itself, before the next starts - on the model that
1834
1895
  // regenerated it: a release follows steps, and a small model thinking little does that well.
1835
1896
  release: ({ plainFile, moduleName, title, onDelta }) => {
1836
- const model = modelFor(chosenModels, "spec_changes", projectModel(from.ctx, plainFile) ?? chatModelOf(from.ctx));
1897
+ const model = releaseModel(from.ctx, plainFile);
1837
1898
  return runReleaseAgent({
1838
1899
  plainFile,
1839
1900
  cwd,
@@ -2173,15 +2234,15 @@ export const JOB_FINISHED_REACTION = [
2173
2234
  " `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
2174
2235
  " project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
2175
2236
  " person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
2176
- "- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
2177
- " shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
2178
- " JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
2179
- " to let you put it in place the way this machine installs things - the exact command, `brew install openjdk@21`,",
2180
- " `mise use -g java@21`, `sdk install maven` - and on yes run it in your shell, check that the command is found now as",
2181
- " the testing script would find it (`command -v`, then the doctor tool, which looks on turri's own PATH), and resume. A tool installed where the PATH turri",
2182
- " was started with does not reach needs turri restarted: say so, with the line for the shell profile. On no, or when it",
2183
- " needs what you cannot do - sudo with a password, a licence, a download behind a login - tell the person exactly what",
2184
- " to do, and that \"resume\" picks it up once it is in.",
2237
+ "- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: you install nothing,",
2238
+ " and change no setting of the machine's or the person's shell - never, even when asked: your shell refuses it. First",
2239
+ " find out, in your shell, whether it is there after all: off the PATH (`command -v`, Homebrew's opt folders,",
2240
+ " /Library/Java/JavaVirtualMachines, a version manager's folder - mise, asdf, sdkman, nvm), or under another name - python",
2241
+ " where the testing script runs python3, of the version the ***plain names. When it is there, the testing script calling",
2242
+ " it the way this machine has it is the fix: change the script, say so in a line, and resume. When it is not, tell the",
2243
+ " person exactly what to run themselves - `brew install openjdk@21`, `mise use -g java@21` - and the line for their",
2244
+ " shell profile when the PATH needs it, and that \"resume\" picks it up once it is in. A tool the person installs where the",
2245
+ " PATH turri was started with does not reach needs turri restarted: say so.",
2185
2246
  "- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
2186
2247
  ].join("\n");
2187
2248
  const sidePanels = new WeakMap();
@@ -2688,6 +2749,8 @@ function registerDeploy(pi, cwd, jobs, state) {
2688
2749
  });
2689
2750
  }
2690
2751
  const MILESTONE_ENTRY = "turri_milestone";
2752
+ /** A newer turri out, found once turri had started (turri-versions.ts). */
2753
+ const VERSION_ENTRY = "turri_version";
2691
2754
  const JOB_ENTRY = "turri_job";
2692
2755
  const JOBS_LIST_ENTRY = "turri_jobs";
2693
2756
  const HISTORY_ENTRY = "turri_history";
@@ -2901,6 +2964,8 @@ function registerBranding(pi, skills) {
2901
2964
  const lines = [
2902
2965
  `${brandPink(theme)("turri")} ${theme.fg("muted", `v${data.version}`)}` +
2903
2966
  (data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
2967
+ // A newer turri out, or one installed and hidden behind this one: said first, under the version it is about.
2968
+ ...(data.versions ?? []).map((line) => theme.fg("warning", line)),
2904
2969
  `${theme.fg("text", "the first coding agent with the power of ")}${brandPink(theme)("regenerative software")}`,
2905
2970
  // Where the settings are changed, without leaving the chat.
2906
2971
  theme.fg("muted", "configure with /turri-config"),
@@ -2912,6 +2977,15 @@ function registerBranding(pi, skills) {
2912
2977
  // where the terminal shows images, drawn in text where it does not.
2913
2978
  return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
2914
2979
  });
2980
+ // A newer turri, found once turri had started: a line, as the banner would have said it.
2981
+ pi.registerEntryRenderer(VERSION_ENTRY, (entry, _options, theme) => {
2982
+ const data = entry.data;
2983
+ if (data === undefined)
2984
+ return undefined;
2985
+ const box = new Box(1, 0);
2986
+ box.addChild(new Text(theme.fg("warning", data.text), 0, 0));
2987
+ return box;
2988
+ });
2915
2989
  let bannered = false;
2916
2990
  pi.on("session_start", (_event, ctx) => {
2917
2991
  if (ctx.hasUI) {
@@ -2924,12 +2998,25 @@ function registerBranding(pi, skills) {
2924
2998
  bannered = true;
2925
2999
  try {
2926
3000
  const identity = turriIdentity();
3001
+ const versions = bannerWarnings();
3002
+ // A newer turri npm says is out only now - the banner read what it said last time: said under the banner once it does.
3003
+ void versions.later.then((line) => {
3004
+ if (line === undefined)
3005
+ return;
3006
+ try {
3007
+ pi.appendEntry(VERSION_ENTRY, { text: line });
3008
+ }
3009
+ catch {
3010
+ /* the chat let go meanwhile: the next start's banner says it */
3011
+ }
3012
+ });
2927
3013
  pi.appendEntry(BANNER_ENTRY, {
2928
3014
  version: identity.version ?? "0.0.0",
2929
3015
  commit: identity.git_commit?.slice(0, 7),
2930
3016
  changed: identity.git_dirty === true,
2931
3017
  pending: skills.pending.length,
2932
3018
  sandbox: sandboxSummary(),
3019
+ versions: versions.lines,
2933
3020
  });
2934
3021
  }
2935
3022
  catch {
@@ -5,6 +5,7 @@ import { type EvaluateOutcome } from "./advisor-chat.ts";
5
5
  import type { JobRegistry } from "./regenerate/jobs.ts";
6
6
  import type { WorkLoop } from "./regenerate/loop.ts";
7
7
  import type { Notice, NoticeAnswer } from "./notice.ts";
8
+ import { type CheckRequest } from "./sentinel-check.ts";
8
9
  /**
9
10
  * turri's session for a host that runs its chats itself - a desktop app, an editor: the chats, each a pi session with
10
11
  * turri in it, on one job registry and work loop. The host shows each chat in its own way - subscribes to its session,
@@ -67,6 +68,19 @@ export interface Turri {
67
68
  * hears each as it is put (`onNotice`) and as it ends (`onNoticeOver`).
68
69
  */
69
70
  notices(): Notice[];
71
+ /**
72
+ * The sentinel asked to check functional specs in a module's release - `funcIds` each `module:func-id`, of the module or
73
+ * of any module its requires chain builds on (a bare func-id is the module's own) - in the module's release, as it tests a release after a pulse: the module's own codebase's last
74
+ * commit, which holds every module its chain builds on, released first unless that is what runs. Queued behind the
75
+ * sentinel's work, in a conversation of its own. Returns the check's id - its notices and its conclusion carry it - or
76
+ * why it was refused: the sentinel off or not started this session, a module not there or that does not parse, a
77
+ * module outside its chain, a func-id a module does not have, one not built in its codebase as its ***plain has it now.
78
+ */
79
+ check(request: CheckRequest): {
80
+ check: string;
81
+ } | {
82
+ refused: string;
83
+ };
70
84
  /**
71
85
  * The test advisor asked to evaluate functional specs, as the chat's `evaluate_tests` does - for a host's own way in,
72
86
  * a button on a functional spec: `module:func-id`s, or func-ids of `plainFile`'s module, even a text judged already and
@@ -6,6 +6,7 @@ import { reachable, ruleNames, settingCandidates } from "./config-command.js";
6
6
  import { settingOf } from "./config-edit.js";
7
7
  import { createTurriCore, sessionAdvisor, sessionLoop, turnOnConformance } from "./extension.js";
8
8
  import { stopDeployed } from "./regenerate/release.js";
9
+ import { prepareCheck } from "./sentinel-check.js";
9
10
  /** turri's session in a host, in the process's working folder - the project's. */
10
11
  export async function createTurri(options = {}) {
11
12
  const { dialogs, uiContext, extensions = [], agentDir = getAgentDir(), sessionDir, restore, ...host } = options;
@@ -71,6 +72,7 @@ export async function createTurri(options = {}) {
71
72
  },
72
73
  decline: async (id) => core.decline(id),
73
74
  notices: () => core.notices(),
75
+ check: (request) => core.check(() => prepareCheck(cwd, request)),
74
76
  evaluateTests: (funcIds, options) => evaluateTests(cwd, sessionAdvisor(core), funcIds, options),
75
77
  turnOnConformanceTests: (plainFile) => turnOnConformance(core, plainFile),
76
78
  settingChoices: (key, typed = "") => {
package/dist/host.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  */
7
7
  export { createTurri, type Turri, type TurriOptions } from "./host-session.ts";
8
8
  export { MAX_CHATS, type ChatInfo, type ChatStatus, type TurriHost } from "./chats.ts";
9
- export { SENTINEL, SENTINEL_START, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState } from "./notice.ts";
9
+ export { SENTINEL, SENTINEL_START, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState, type SentinelCheck } from "./notice.ts";
10
+ export type { CheckRequest } from "./sentinel-check.ts";
10
11
  export type { AskUserIO } from "./ask.ts";
11
12
  /** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
12
13
  export * as pi from "@earendil-works/pi-coding-agent";
@@ -16,6 +17,8 @@ export { configSettings, currentValue, EFFECT_TEXT, overriddenBy, readConfigText
16
17
  export { bedrockRegionFor, type BedrockRegion, type BedrockRegionSource } from "./bedrock-region.ts";
17
18
  export { BEDROCK_DEFAULT, BEDROCK_GEOGRAPHIES, BEDROCK_IN_REGION, BEDROCK_REGIONS, bedrockAwsRegion, bedrockDefaultModels, bedrockMakers, bedrockModels, bedrockModelsIn, bedrockNeedsAwsRegion, bedrockPlaceOf, initConfig, makerLabel, VENDORS, type BedrockChoice, type BedrockMaker, type BedrockOffer, type InitConfigOptions, type ModelChoice, type ModelSet, type Vendor, } from "./init-config.ts";
18
19
  export { piModelCatalogue, type CatalogueModel, type ModelCatalogue } from "./model-check.ts";
20
+ export { EXTENSION_PROVIDERS, installedModelRuntime } from "./installed-providers.ts";
21
+ export { machineChange, machineChangeReason } from "./regenerate/command-check.ts";
19
22
  export { doctor, type Check, type DoctorOptions, type Level } from "./setup.ts";
20
23
  export { checkPlainFile, type CheckResult } from "./plain/check.ts";
21
24
  export { plainFileParser } from "./plain/parser.ts";
@@ -26,6 +29,8 @@ export { conceptBeingTyped, findConcepts } from "./plain/concepts.ts";
26
29
  export { plainGraph, type PlainConceptNode, type PlainGraph, type PlainItemNode, type PlainLink, type PlainModuleNode, type PlainScenario, type PlainSection, type PlainTestPlainFileNode } from "./plain/graph.ts";
27
30
  export { isStack, STDLIB_KINDS, stdlibTemplates, type StdlibKind, type StdlibTemplate } from "./stdlib.ts";
28
31
  export { createWorkLoop, describeModules, functionalityCompletion, promptOf, type DriftReport, type ModuleStatus, type WorkLoop } from "./regenerate/loop.ts";
32
+ export { projectStanding, type ModuleStanding, type StandingOptions } from "./regenerate/standing.ts";
33
+ export { requiresOrder } from "./regenerate/requires.ts";
29
34
  export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry, type PulseCases, type PulseSpec } from "./regenerate/jobs.ts";
30
35
  export { describeTurnIncident, onModelEvent, type TranscriptEntry } from "./regenerate/activity.ts";
31
36
  export { sweepOrphanedGroups } from "./regenerate/shell.ts";
package/dist/host.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // The session and its chats.
8
8
  export { createTurri } from "./host-session.js";
9
9
  export { MAX_CHATS } from "./chats.js";
10
- // The sentinel's notices: what it puts to the person, and how they answer.
10
+ // The sentinel's notices: what it puts to the person, and how they answer - and the checks a host asks of it.
11
11
  export { SENTINEL, SENTINEL_START } from "./notice.js";
12
12
  /** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
13
13
  export * as pi from "@earendil-works/pi-coding-agent";
@@ -24,6 +24,8 @@ export { bedrockRegionFor } from "./bedrock-region.js";
24
24
  export { BEDROCK_DEFAULT, BEDROCK_GEOGRAPHIES, BEDROCK_IN_REGION, BEDROCK_REGIONS, bedrockAwsRegion, bedrockDefaultModels, bedrockMakers, bedrockModels, bedrockModelsIn, bedrockNeedsAwsRegion, bedrockPlaceOf, initConfig, makerLabel, VENDORS, } from "./init-config.js";
25
25
  // pi's model catalogue, read without a request: what initConfig checks the models given against, and what a host offers.
26
26
  export { piModelCatalogue } from "./model-check.js";
27
+ export { EXTENSION_PROVIDERS, installedModelRuntime } from "./installed-providers.js";
28
+ export { machineChange, machineChangeReason } from "./regenerate/command-check.js";
27
29
  // Whether this machine, and a project on it, is ready for turri - as `turri doctor` checks it, printing nothing.
28
30
  export { doctor } from "./setup.js";
29
31
  // A spec: parsed, checked, coloured.
@@ -41,6 +43,8 @@ export { plainGraph } from "./plain/graph.js";
41
43
  export { isStack, STDLIB_KINDS, stdlibTemplates } from "./stdlib.js";
42
44
  // The work: what is built, what is left, and the jobs that build it.
43
45
  export { createWorkLoop, describeModules, functionalityCompletion, promptOf } from "./regenerate/loop.js";
46
+ export { projectStanding } from "./regenerate/standing.js";
47
+ export { requiresOrder } from "./regenerate/requires.js";
44
48
  export { createJobRegistry, defaultParallelJobs } from "./regenerate/jobs.js";
45
49
  export { describeTurnIncident, onModelEvent } from "./regenerate/activity.js";
46
50
  export { sweepOrphanedGroups } from "./regenerate/shell.js";
@@ -16,7 +16,7 @@ import { type CatalogueModel, type ModelCatalogue } from "./model-check.ts";
16
16
  * new one takes from a config.yaml beside it the keys turri reads too.
17
17
  */
18
18
  /** A vendor whose models the quick start config is filled in with. */
19
- export type Vendor = "anthropic" | "openai" | "google" | "amazon-bedrock" | "other";
19
+ export type Vendor = "anthropic" | "openai" | "google" | "amazon-bedrock" | "claude-code" | "other";
20
20
  /** Whose models Amazon Bedrock serves: the maker a model's id names after its geography - anthropic, openai, xai, meta... */
21
21
  export type BedrockMaker = string;
22
22
  /**
@@ -7,6 +7,7 @@ import { adoptLegacyConfig, CONFIG_FILE, CONFIG_KEYS, LEGACY_CONFIG_FILE, ROLE_T
7
7
  import { withTopLevelValue } from "./config-edit.js";
8
8
  import { brand } from "./brand.js";
9
9
  import { closestModels, piModelCatalogue } from "./model-check.js";
10
+ import { EXTENSION_PROVIDERS } from "./installed-providers.js";
10
11
  /** The geographies turri's own pair for each maker is served in - the first the default. Every other: bedrockMakers. */
11
12
  export const BEDROCK_REGIONS = {
12
13
  anthropic: ["global", "us", "eu", "jp", "au"],
@@ -152,6 +153,14 @@ export const VENDORS = {
152
153
  ...bedrockModels({ maker: "anthropic", region: BEDROCK_DEFAULT.region }),
153
154
  keys: [{ vendor: "AWS", variable: "AWS_PROFILE" }],
154
155
  },
156
+ // A Claude Pro or Max subscription, through Claude Code: its pi extension's provider (installed-providers.ts), no key.
157
+ "claude-code": {
158
+ label: "Claude subscription",
159
+ harness: "claude-code/claude-opus-5-5",
160
+ coding: "claude-code/claude-sonnet-5",
161
+ strong: "claude-code/claude-opus-5-5",
162
+ keys: [],
163
+ },
155
164
  // A mix - the strongest to talk to, a cheap one to code - and the starting point for any other provider pi supports.
156
165
  other: {
157
166
  label: "Other",
@@ -370,6 +379,8 @@ export function vendorOf(answer) {
370
379
  const byNumber = /^\d+$/.test(typed) ? names[Number(typed) - 1] : undefined;
371
380
  if (typed === "bedrock" || typed === "aws")
372
381
  return "amazon-bedrock";
382
+ if (typed === "claude" || typed === "subscription" || typed === "claude code")
383
+ return "claude-code";
373
384
  return byNumber ?? names.find((name) => name === typed || VENDORS[name].label.toLowerCase() === typed);
374
385
  }
375
386
  /** The makers offered, as numbered: pi's catalogue's, or without it turri's two. */
@@ -404,6 +415,17 @@ function shellProfile() {
404
415
  function signInText(vendor, bedrock, models) {
405
416
  // Models given on another vendor's terms - other's, a provider of their own: the keys of the providers they name.
406
417
  const providers = models !== undefined && vendor === "other" ? [...new Set([models.harness, models.coding].map((model) => model.split("/")[0]))] : [];
418
+ // A provider a pi extension brings - a Claude subscription through Claude Code: no key, its extension and its own sign-in.
419
+ const extensions = (vendor === "other" ? providers : [vendor]).flatMap((provider) => (EXTENSION_PROVIDERS[provider] !== undefined ? [EXTENSION_PROVIDERS[provider]] : []));
420
+ if (extensions.length > 0 && (vendor !== "other" || extensions.length === providers.length)) {
421
+ return extensions.flatMap((extension) => [
422
+ brand.bold(`Set up ${extension.label}`),
423
+ ` ${brand.accent(extension.install)} ${brand.muted("its pi extension: turri finds what pi has installed")}`,
424
+ ` ${brand.muted(extension.signIn)}`,
425
+ "",
426
+ brand.muted("turri doctor --models checks it answers."),
427
+ ]);
428
+ }
407
429
  const keys = providers.length > 0
408
430
  ? providers.flatMap((provider) => (provider in VENDORS && provider !== "other" && provider !== "amazon-bedrock" ? VENDORS[provider].keys : [{ vendor: makerLabel(provider), variable: "" }]))
409
431
  : VENDORS[vendor].keys;
@@ -451,6 +473,8 @@ function whose(vendor, models, given) {
451
473
  const makers = [...new Set(places.map((place) => `${makerLabel(place.maker)}'s`))];
452
474
  return `${and(makers)} models on Amazon Bedrock (${[...new Set(places.map((place) => place.geography))].join(", ")})`;
453
475
  }
476
+ if (vendor === "claude-code")
477
+ return "your Claude subscription's models, through Claude Code";
454
478
  if (vendor !== "other")
455
479
  return `${VENDORS[vendor].label}'s models`;
456
480
  if (!given)
@@ -470,8 +494,12 @@ function notInCatalogue(spec, catalogue) {
470
494
  if (model === undefined) {
471
495
  if (THINKING.test(id) && known.some((entry) => entry.id === id.replace(THINKING, "")))
472
496
  return `${spec} names a thinking level: give the model alone, ${spec.replace(THINKING, "")} - turri writes how hard each thinks.`;
473
- if (known.length === 0)
474
- return `${spec} is not in pi's model catalogue: pi knows no provider ${provider}.`;
497
+ if (known.length === 0) {
498
+ const extension = EXTENSION_PROVIDERS[provider];
499
+ return extension !== undefined
500
+ ? `${spec} is not in pi's model catalogue: ${provider} - ${extension.label} - comes with a pi extension that is not installed: ${extension.install}`
501
+ : `${spec} is not in pi's model catalogue: pi knows no provider ${provider}.`;
502
+ }
475
503
  const close = closestModels(id, known.filter((entry) => entry.type === "chat").map((entry) => entry.id));
476
504
  return `${spec} is not in pi's model catalogue${close.length > 0 ? ` - did you mean ${close.map((other) => `${provider}/${other}`).join(", ")}?` : "."}`;
477
505
  }
@@ -662,7 +690,9 @@ async function askVendor(given, catalogue) {
662
690
  const { label, harness, coding } = VENDORS[name];
663
691
  const what = name === "amazon-bedrock"
664
692
  ? "Anthropic's, OpenAI's, xAI's, Meta's... models on AWS"
665
- : `${name === "other" ? "a mix - " : ""}${harness.split("/")[1]} to talk to, ${coding.split("/")[1]} to code`;
693
+ : name === "claude-code"
694
+ ? `your Claude Pro or Max, through Claude Code - ${harness.split("/")[1]} to talk to, ${coding.split("/")[1]} to code`
695
+ : `${name === "other" ? "a mix - " : ""}${harness.split("/")[1]} to talk to, ${coding.split("/")[1]} to code`;
666
696
  return [label, what];
667
697
  }));
668
698
  vendor = await askUntil(rl, choose(names.length), vendorOf, `Type a number from 1 to ${names.length}, or a vendor's name.`);
@@ -0,0 +1,40 @@
1
+ import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * The model providers the person's pi extensions register - a Claude subscription through Claude Code
4
+ * (pi-claude-code, `claude-code`), a company's own gateway - wherever turri needs a model and no conversation has them.
5
+ *
6
+ * In a chat, pi loads the person's installed pi packages itself, and turri's agents take the providers from the
7
+ * conversation's model registry (pi.ts `inheritProviders`). A command with no conversation - `turri regenerate`,
8
+ * `turri change-plain`, doctor's model check, init-config - has none: here they are loaded the way pi loads them, once a
9
+ * process, and only the providers they register are kept. turri's own extension, when an older turri installed it into
10
+ * every pi, is loaded as nothing meanwhile (`LOADING_PROVIDERS`): it would start a session of its own.
11
+ */
12
+ /** Set while the person's extensions are loaded for their providers: turri's own extension does nothing then. */
13
+ export declare const LOADING_PROVIDERS = "TURRI_LOADING_PROVIDERS";
14
+ /**
15
+ * pi's own providers that come from an extension rather than with pi: what installs each, and how it is signed in to - for
16
+ * doctor and init-config to say when a config names one that is not there.
17
+ */
18
+ export declare const EXTENSION_PROVIDERS: Record<string, {
19
+ label: string;
20
+ install: string;
21
+ signIn: string;
22
+ }>;
23
+ /**
24
+ * The providers the person's installed pi extensions register, for `cwd` - its project's pi settings with the person's
25
+ * own - loaded once. Undefined when none could be loaded: pi's built-in providers and models.json are what there is.
26
+ */
27
+ export declare function installedProviders(cwd?: string): Promise<ModelRegistry | undefined>;
28
+ /**
29
+ * pi's model runtime as turri's agents have it with no conversation: pi's providers, models.json and auth.json, and the
30
+ * providers the person's installed pi extensions register - for a host that signs in or lists models before a chat is
31
+ * open, whose own `ModelRuntime.create()` would have none of an extension's (`claude-code`, say).
32
+ */
33
+ export declare function installedModelRuntime(cwd?: string): Promise<ModelRuntime>;
34
+ /** For tests: forget what was loaded. */
35
+ export declare function forgetInstalledProviders(): void;
36
+ /**
37
+ * pi's model runtime - its catalogue, models.json and its credentials - with `source`'s providers registered in it: a
38
+ * conversation's, or the person's installed ones. Its models are listed as the registrations land.
39
+ */
40
+ export declare function runtimeWithProviders(source: ModelRegistry | undefined): Promise<ModelRuntime>;
@@ -0,0 +1,98 @@
1
+ import { join } from "node:path";
2
+ import { createAgentSessionServices, getAgentDir, ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+ /**
4
+ * The model providers the person's pi extensions register - a Claude subscription through Claude Code
5
+ * (pi-claude-code, `claude-code`), a company's own gateway - wherever turri needs a model and no conversation has them.
6
+ *
7
+ * In a chat, pi loads the person's installed pi packages itself, and turri's agents take the providers from the
8
+ * conversation's model registry (pi.ts `inheritProviders`). A command with no conversation - `turri regenerate`,
9
+ * `turri change-plain`, doctor's model check, init-config - has none: here they are loaded the way pi loads them, once a
10
+ * process, and only the providers they register are kept. turri's own extension, when an older turri installed it into
11
+ * every pi, is loaded as nothing meanwhile (`LOADING_PROVIDERS`): it would start a session of its own.
12
+ */
13
+ /** Set while the person's extensions are loaded for their providers: turri's own extension does nothing then. */
14
+ export const LOADING_PROVIDERS = "TURRI_LOADING_PROVIDERS";
15
+ /**
16
+ * pi's own providers that come from an extension rather than with pi: what installs each, and how it is signed in to - for
17
+ * doctor and init-config to say when a config names one that is not there.
18
+ */
19
+ export const EXTENSION_PROVIDERS = {
20
+ "claude-code": {
21
+ label: "Claude subscription (Claude Code)",
22
+ install: "pi install https://github.com/earendil-works/pi-claude-code",
23
+ signIn: "Claude Code installed (npm install -g @anthropic-ai/claude-code) and signed in to your Claude Pro or Max subscription: claude auth login",
24
+ },
25
+ };
26
+ const loaded = new Map();
27
+ /**
28
+ * The providers the person's installed pi extensions register, for `cwd` - its project's pi settings with the person's
29
+ * own - loaded once. Undefined when none could be loaded: pi's built-in providers and models.json are what there is.
30
+ */
31
+ export function installedProviders(cwd = process.cwd()) {
32
+ let found = loaded.get(cwd);
33
+ if (found === undefined) {
34
+ found = load(cwd);
35
+ loaded.set(cwd, found);
36
+ }
37
+ return found;
38
+ }
39
+ async function load(cwd) {
40
+ const before = process.env[LOADING_PROVIDERS];
41
+ process.env[LOADING_PROVIDERS] = "1";
42
+ try {
43
+ // Only the extensions: no skills, prompts, themes or context files are read for this.
44
+ const { modelRuntime } = await createAgentSessionServices({
45
+ cwd,
46
+ resourceLoaderOptions: { noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true },
47
+ });
48
+ const registry = new ModelRegistry(modelRuntime);
49
+ return registry.getRegisteredProviderIds().length > 0 ? registry : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ finally {
55
+ if (before === undefined)
56
+ delete process.env[LOADING_PROVIDERS];
57
+ else
58
+ process.env[LOADING_PROVIDERS] = before;
59
+ }
60
+ }
61
+ /**
62
+ * pi's model runtime as turri's agents have it with no conversation: pi's providers, models.json and auth.json, and the
63
+ * providers the person's installed pi extensions register - for a host that signs in or lists models before a chat is
64
+ * open, whose own `ModelRuntime.create()` would have none of an extension's (`claude-code`, say).
65
+ */
66
+ export async function installedModelRuntime(cwd = process.cwd()) {
67
+ return runtimeWithProviders(await installedProviders(cwd));
68
+ }
69
+ /** For tests: forget what was loaded. */
70
+ export function forgetInstalledProviders() {
71
+ loaded.clear();
72
+ }
73
+ /**
74
+ * pi's model runtime - its catalogue, models.json and its credentials - with `source`'s providers registered in it: a
75
+ * conversation's, or the person's installed ones. Its models are listed as the registrations land.
76
+ */
77
+ export async function runtimeWithProviders(source) {
78
+ const agentDir = getAgentDir();
79
+ const runtime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
80
+ if (source === undefined)
81
+ return runtime;
82
+ const registry = new ModelRegistry(runtime);
83
+ for (const id of source.getRegisteredProviderIds()) {
84
+ const native = source.getRegisteredNativeProvider(id);
85
+ const config = source.getRegisteredProviderConfig(id);
86
+ try {
87
+ if (native !== undefined)
88
+ registry.registerProvider(native);
89
+ else if (config !== undefined)
90
+ registry.registerProvider(id, config);
91
+ }
92
+ catch {
93
+ // One provider that will not register leaves the others, and pi's own, usable.
94
+ }
95
+ }
96
+ await runtime.refresh({ allowNetwork: false });
97
+ return runtime;
98
+ }