@turri-dev/cli 0.1.27 → 0.2.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 +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +70 -14
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -13
- package/dist/regenerate/conformance.js +119 -82
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +62 -43
- package/dist/regenerate/loop.js +472 -341
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +71 -28
- package/dist/regenerate/test-kinds.js +110 -63
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +87 -52
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +23 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
package/dist/extension.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Whether the terminal shows images, before anything draws: the banner's jellyfish is a picture or text.
|
|
2
2
|
import "./terminal-images.js";
|
|
3
|
+
import { moduleOfTestPlainFile, refuseTestPlainFile, testPlainFileRefusal } from "./test-plain-files.js";
|
|
3
4
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
6
|
import { conversationCommand, conversationSettled, conversationWriting } from "./regenerate/conversation-writes.js";
|
|
@@ -8,12 +9,16 @@ import { allocateImageId, Box, getCapabilities, getPngDimensions, isKeyRelease,
|
|
|
8
9
|
import { defineTool, getMarkdownTheme, UserMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
10
11
|
import { CODING_MODEL_ENV } from "./chat-models.js";
|
|
11
|
-
import { AGENT_ROLES, CONFIG_FILE, configForSpec, hasProjectConfig, LEGACY_CONFIG_FILE, projectConfigFile, stackConfigFileIn, stackOf, stacksOf, withStack, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, splitModelSpec } from "./config.js";
|
|
12
|
-
import { skillRunDir } from "./regenerate/paths.js";
|
|
12
|
+
import { AGENT_ROLES, chosenStack, CONFIG_FILE, configForSpec, stackForModule, hasProjectConfig, LEGACY_CONFIG_FILE, projectConfigFile, stackConfigFileIn, stackOf, stacksOf, withStack, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, splitModelSpec } from "./config.js";
|
|
13
|
+
import { projectDirOf, skillRunDir } from "./regenerate/paths.js";
|
|
13
14
|
import { sweepOrphanedGroups } from "./regenerate/shell.js";
|
|
14
15
|
import { readScript, scriptAgainstMachine, stackOnMachine, toolchainsBlock } from "./stack.js";
|
|
15
16
|
import { sandboxAvailable, sandboxSummary } from "./regenerate/os-sandbox.js";
|
|
16
|
-
import { findTestScripts } from "./regenerate/test-scripts.js";
|
|
17
|
+
import { findTestScripts, scriptKindOfSkill } from "./regenerate/test-scripts.js";
|
|
18
|
+
import { writingScript } from "./regenerate/scripts-being-written.js";
|
|
19
|
+
import { applyBedrockRegion } from "./bedrock-region.js";
|
|
20
|
+
import { conformanceBlock, conformanceTestsOnIn, CONFORMANCE_TOOLS, TURN_ON_TOOL, turnOnConformanceTests, turnOnConformanceTestsTool } from "./conformance-switch.js";
|
|
21
|
+
import { BACKGROUND_NOTE, doneLine, failedTask, ownChanges, runDetails, skillRunDelivery, startedText } from "./background-skills.js";
|
|
17
22
|
import { createAskUserTool } from "./tools.js";
|
|
18
23
|
import { asksFromScratch, asksWholeChain, driftTool, fromWorkingDir, haltTool, loopReply, MODEL_PARAM, regenerateTool, STACK_PARAM, stopJob, WAIT_PARAM, withCallStack, WORKING_DIR_PARAM } from "./loop-tools.js";
|
|
19
24
|
import { createWebTools } from "./regenerate/web.js";
|
|
@@ -27,10 +32,14 @@ import { createSteeringStore, EVERY_MODULE, moduleNameOf, queueSectionOf, steeri
|
|
|
27
32
|
import { askedForCommandLine, refusal, selfInvocation, TOOLS_NOT_CLI } from "./self-invocation.js";
|
|
28
33
|
import { NEW_PROJECT } from "./new-project.js";
|
|
29
34
|
import { describeChoices, describeResolution, modelFor, resolveModel } from "./regenerate/engine.js";
|
|
30
|
-
import { checkPlainFile,
|
|
35
|
+
import { checkPlainFile, formatCheckResults } from "./plain/check.js";
|
|
36
|
+
import { checkPlainTool } from "./parse-tool.js";
|
|
31
37
|
import { changedFiles, projectFiles } from "./plain-files.js";
|
|
32
38
|
import { whyFailed } from "./regenerate/diagnose-job.js";
|
|
33
39
|
import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
|
|
40
|
+
import { createTestAdvisor } from "./regenerate/advisor-runs.js";
|
|
41
|
+
import { advisorDir } from "./regenerate/advisor-records.js";
|
|
42
|
+
import { bindAdvisor, evaluateTestsTool, TEST_ADVISOR_BLOCK, verdictDelivery } from "./advisor-chat.js";
|
|
34
43
|
import { brandNeon, brandPink, plainPaint } from "./regenerate/plain-palette.js";
|
|
35
44
|
import { halfBlocks, JELLYFISH_FRAME_MS, JELLYFISH_REST, JELLYFISH_SEQUENCE, JELLYFISH_TEXT_FRAMES } from "./jellyfish.js";
|
|
36
45
|
import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
|
|
@@ -54,7 +63,7 @@ import { describeSpecConflict } from "./regenerate/stop-report.js";
|
|
|
54
63
|
import { inheritProviders, inheritThinkingLevel } from "./pi.js";
|
|
55
64
|
import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
|
|
56
65
|
import { loadRules, renderRulesBlock } from "./rules.js";
|
|
57
|
-
import { registerTurriConfig } from "./config-command.js";
|
|
66
|
+
import { registerTurriConfig, ruleNames } from "./config-command.js";
|
|
58
67
|
import { writeSetting } from "./config-edit.js";
|
|
59
68
|
import { describeStdlib } from "./stdlib.js";
|
|
60
69
|
import { DECLINED, peekKeyListener, runAsk } from "./ask.js";
|
|
@@ -70,7 +79,6 @@ import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
|
|
|
70
79
|
import { currentChat } from "./chat-scope.js";
|
|
71
80
|
import { registerChats } from "./chat-commands.js";
|
|
72
81
|
import { turriIdentity } from "./environment.js";
|
|
73
|
-
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
74
82
|
import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
|
|
75
83
|
import { createJobRegistry, defaultParallelJobs, describeJob, firstRegeneration, formatDuration, formatElapsed, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
|
|
76
84
|
/**
|
|
@@ -274,6 +282,8 @@ const hosts = new WeakMap();
|
|
|
274
282
|
export function createTurriCore(host = {}, options = {}) {
|
|
275
283
|
dropNoise();
|
|
276
284
|
const cwd = process.cwd();
|
|
285
|
+
// The AWS region the project's Bedrock models need, before any model is called - a host has no shell to export it in.
|
|
286
|
+
applyBedrockRegion(cwd);
|
|
277
287
|
let core;
|
|
278
288
|
// Finished work is announced twice, to two audiences, because pi treats them differently - see ChatInstance.announce:
|
|
279
289
|
// in the chat that asked for it, or the one shown when that is closed.
|
|
@@ -337,6 +347,30 @@ export function createTurriCore(host = {}, options = {}) {
|
|
|
337
347
|
setSurface(jobs, host.surface);
|
|
338
348
|
// What release has shipped, module by module: release does nothing for a module whose last commit it already shipped.
|
|
339
349
|
deployStates.set(jobs, createDeployState());
|
|
350
|
+
// The test advisor watches what the work loop looks at, and picks up what it left pending when turri last quit.
|
|
351
|
+
if (options.advisor === true) {
|
|
352
|
+
// The conversation's own words on how the work is to be planned - AGENTS.md the advisor reads itself.
|
|
353
|
+
const context = (plainFile) => [...steeringStore.get(EVERY_MODULE), ...steeringStore.get(moduleNameOf(plainFile))];
|
|
354
|
+
const advisor = createTestAdvisor({
|
|
355
|
+
context,
|
|
356
|
+
// A verdict goes to the chat the person looks at - a host's too - which writes the scenarios on a yes.
|
|
357
|
+
onVerdict: (verdict) => void session.advised(verdict),
|
|
358
|
+
});
|
|
359
|
+
advisors.set(jobs, advisor);
|
|
360
|
+
// Where it may have left work pending: the folder turri runs in, and a project in a folder of it - ***plain kept in plain/.
|
|
361
|
+
const projects = () => {
|
|
362
|
+
try {
|
|
363
|
+
return [cwd, ...readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && existsSync(advisorDir(join(cwd, entry.name)))).map((entry) => join(cwd, entry.name))];
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return [cwd];
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
for (const project of projects())
|
|
371
|
+
advisor.resume(project);
|
|
372
|
+
}, 0).unref?.();
|
|
373
|
+
}
|
|
340
374
|
// What this session does, as facts, in the project's .turri/ledger - a pulse's own in its project. Said once, in the
|
|
341
375
|
// chat shown, when it cannot be written.
|
|
342
376
|
if (ledgerWanted()) {
|
|
@@ -469,6 +503,8 @@ function turriExtension(api, session, link) {
|
|
|
469
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" });
|
|
470
504
|
};
|
|
471
505
|
const notices = noticeDialogs(pi, session, link, contextNow);
|
|
506
|
+
// The test advisor's verdicts, once the chat is idle: a yes starts a turn that writes its scenarios, a no is a line.
|
|
507
|
+
const verdicts = verdictDelivery(pi, link, contextNow);
|
|
472
508
|
link.live({
|
|
473
509
|
announce,
|
|
474
510
|
landed,
|
|
@@ -476,12 +512,14 @@ function turriExtension(api, session, link) {
|
|
|
476
512
|
notice: notices.put,
|
|
477
513
|
noticeOver: notices.over,
|
|
478
514
|
concluded: notices.concluded,
|
|
515
|
+
advised: verdicts.put,
|
|
479
516
|
act: (task) => {
|
|
480
517
|
void pi.sendMessage({ customType: "turri_notice_answer", content: task, display: false }, { triggerTurn: true, deliverAs: "followUp" });
|
|
481
518
|
},
|
|
482
519
|
jobsChanged: () => panel.refresh(),
|
|
483
520
|
thinkingLevel: () => pi.getThinkingLevel(),
|
|
484
521
|
context: contextNow,
|
|
522
|
+
conformanceSwitched: () => conformanceTools(),
|
|
485
523
|
});
|
|
486
524
|
pi.on("session_start", (_event, ctx) => {
|
|
487
525
|
lastContext = ctx;
|
|
@@ -529,7 +567,18 @@ function turriExtension(api, session, link) {
|
|
|
529
567
|
return { systemPrompt: host.systemPrompt !== undefined ? host.systemPrompt(prompt) : prompt };
|
|
530
568
|
}
|
|
531
569
|
return {
|
|
532
|
-
systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [
|
|
570
|
+
systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [
|
|
571
|
+
TOOLS_NOT_CLI,
|
|
572
|
+
NEW_PROJECT,
|
|
573
|
+
pulseEndsBlock(session.sentinelOn()),
|
|
574
|
+
// Conformance tests - what they are, whether they are on here, and how they are turned on: every turn, read afresh.
|
|
575
|
+
conformanceBlock(conformanceTestsOnIn(cwd)),
|
|
576
|
+
// The test advisor, and that the chat alone writes scenarios - while the session has one the project wants.
|
|
577
|
+
...(sessionAdvisor(session) !== undefined && conformanceTestsOnIn(cwd) && readProjectConfig(cwd).testAdvisor !== false ? [TEST_ADVISOR_BLOCK] : []),
|
|
578
|
+
projectConfigBlock(cwd),
|
|
579
|
+
machineBlock(cwd, machine.now()),
|
|
580
|
+
rulesBlock(),
|
|
581
|
+
]),
|
|
533
582
|
};
|
|
534
583
|
});
|
|
535
584
|
// The conversation's agent does not run turri again through its shell, unless the person
|
|
@@ -641,6 +690,33 @@ function turriExtension(api, session, link) {
|
|
|
641
690
|
registerSteering(pi, cwd);
|
|
642
691
|
registerSideQuestions(pi, cwd, jobs);
|
|
643
692
|
registerStacks(pi, cwd);
|
|
693
|
+
// The chat's own way to the test advisor, while the session has one - whatever test-advisor says.
|
|
694
|
+
if (sessionAdvisor(session) !== undefined)
|
|
695
|
+
pi.registerTool(evaluateTestsTool({ cwd, advisor: () => sessionAdvisor(session) }));
|
|
696
|
+
// Conformance tests on or off: conform and evaluate_tests are the chat's while they are on, the tool that turns them on
|
|
697
|
+
// while they are off - set as each turn starts, and at once when the chat turns them on. Not the sentinel's: it has its own.
|
|
698
|
+
const conformanceTools = () => {
|
|
699
|
+
if (isSentinel)
|
|
700
|
+
return;
|
|
701
|
+
try {
|
|
702
|
+
const on = conformanceTestsOnIn(cwd);
|
|
703
|
+
const registered = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
704
|
+
const active = pi.getActiveTools();
|
|
705
|
+
const wanted = [
|
|
706
|
+
...active.filter((name) => !CONFORMANCE_TOOLS.includes(name) && name !== TURN_ON_TOOL),
|
|
707
|
+
...(on ? CONFORMANCE_TOOLS : [TURN_ON_TOOL]).filter((name) => registered.has(name)),
|
|
708
|
+
];
|
|
709
|
+
if (wanted.length !== active.length || wanted.some((name) => !active.includes(name)))
|
|
710
|
+
pi.setActiveTools(wanted);
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
/* not bound to a session yet: the next turn sets them */
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
if (!isSentinel)
|
|
717
|
+
pi.registerTool(turnOnConformanceTestsTool({ turnOn: (plainFile) => turnOnConformance(session, plainFile) }));
|
|
718
|
+
pi.on("session_start", () => conformanceTools());
|
|
719
|
+
pi.on("before_agent_start", () => conformanceTools());
|
|
644
720
|
// More chats beside this one, in the terminal: /chat, alt+n, alt+1…9 (chat-commands.ts).
|
|
645
721
|
if (session.terminal)
|
|
646
722
|
registerChats(pi, session);
|
|
@@ -660,6 +736,76 @@ function turriExtension(api, session, link) {
|
|
|
660
736
|
},
|
|
661
737
|
});
|
|
662
738
|
}
|
|
739
|
+
// A skill that runs in the background: started, and the turn goes on; its end comes back to this chat once it is idle.
|
|
740
|
+
const skillRunEnds = skillRunDelivery(pi, link, contextNow);
|
|
741
|
+
pi.on("session_shutdown", (event) => {
|
|
742
|
+
if (event.reason === "quit")
|
|
743
|
+
for (const controller of backgroundSkillRuns.values())
|
|
744
|
+
controller.abort();
|
|
745
|
+
});
|
|
746
|
+
const startInBackground = (skill, params, plainFile, ctx) => {
|
|
747
|
+
const key = `${cwd}\0${skill.name}`;
|
|
748
|
+
if (backgroundSkillRuns.has(key)) {
|
|
749
|
+
return { text: `${skill.name} is running in the background already; what it does comes back to this chat when it ends. Nothing new was started.`, details: { background: true, started: false } };
|
|
750
|
+
}
|
|
751
|
+
const model = skill.model === "coding" ? jobModel(ctx, "spec_changes", undefined, plainFile) : conversationModel(ctx);
|
|
752
|
+
const stack = plainFile !== undefined ? stackForModule(plainFile) : chosenStack();
|
|
753
|
+
const kind = scriptKindOfSkill(skill.name);
|
|
754
|
+
const shown = plainFile !== undefined ? relative(cwd, plainFile) || basename(plainFile) : undefined;
|
|
755
|
+
// While it writes a testing script, the modules that need it wait for it rather than fail for want of it.
|
|
756
|
+
const written = kind !== undefined ? writingScript({ project: plainFile !== undefined ? projectDirOf(plainFile) : cwd, kind, ...(stack !== undefined ? { stack } : {}) }) : () => { };
|
|
757
|
+
const controller = new AbortController();
|
|
758
|
+
backgroundSkillRuns.set(key, controller);
|
|
759
|
+
const context = [
|
|
760
|
+
conversationContext(ctx, params.context),
|
|
761
|
+
shown !== undefined ? `It is for ${shown}${stack !== undefined ? `, on the stack ${stack}` : ""}.` : undefined,
|
|
762
|
+
BACKGROUND_NOTE,
|
|
763
|
+
]
|
|
764
|
+
.filter((part) => part !== undefined && part.trim() !== "")
|
|
765
|
+
.join("\n\n");
|
|
766
|
+
const filesBefore = projectFiles(cwd);
|
|
767
|
+
const writes = ["write", "edit", "bash", "change_plain"].some((tool) => skill.allowedTools.includes(tool));
|
|
768
|
+
const quiet = { step: () => { }, artifact: () => { }, gate: () => { }, judge: () => { }, outcome: () => { } };
|
|
769
|
+
const about = runDetails(kind, shown);
|
|
770
|
+
const end = (ended) => skillRunEnds.put(ended.ok
|
|
771
|
+
? { skill: skill.name, ok: true, line: doneLine(skill.name, ended.changed, kind), ...about }
|
|
772
|
+
: { skill: skill.name, ok: false, line: `${skill.name} ${ended.why}.`, task: failedTask(skill.name, ended.why, ended.output, kind), ...about });
|
|
773
|
+
void captureConsole(() => withStack(stack, () => runSkill({
|
|
774
|
+
skillDir: skill.dir,
|
|
775
|
+
signal: controller.signal,
|
|
776
|
+
cwd,
|
|
777
|
+
runDir: skillRunDir(cwd, skill.name),
|
|
778
|
+
report: quiet,
|
|
779
|
+
context,
|
|
780
|
+
...(model !== undefined ? { model } : {}),
|
|
781
|
+
// Nobody is asked: a gate that cannot be satisfied stands.
|
|
782
|
+
escalate: async () => false,
|
|
783
|
+
})))
|
|
784
|
+
.then(({ result }) => {
|
|
785
|
+
// Its own: the chat went on meanwhile, and what it wrote is not the run's.
|
|
786
|
+
const changed = ownChanges(changedFiles(filesBefore, projectFiles(cwd, filesBefore.keys())), kind);
|
|
787
|
+
if (!result.ok)
|
|
788
|
+
end({ ok: false, why: "stopped at a review gate, with no approved artifact", output: result.output });
|
|
789
|
+
else if (writes && changed.length === 0)
|
|
790
|
+
end({ ok: false, why: "ended having changed no file in the project", output: result.output });
|
|
791
|
+
else
|
|
792
|
+
end({ ok: true, changed });
|
|
793
|
+
})
|
|
794
|
+
.catch((error) => {
|
|
795
|
+
// Stopped as turri quits: nobody is left to tell.
|
|
796
|
+
if (error instanceof JobStoppedError)
|
|
797
|
+
return;
|
|
798
|
+
end({ ok: false, why: `failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
799
|
+
})
|
|
800
|
+
.finally(() => {
|
|
801
|
+
backgroundSkillRuns.delete(key);
|
|
802
|
+
written();
|
|
803
|
+
});
|
|
804
|
+
return {
|
|
805
|
+
text: startedText(skill.name, { ...(model !== undefined ? { model } : {}), ...(shown !== undefined ? { plainFile: shown } : {}), ...(kind !== undefined ? { kind } : {}) }),
|
|
806
|
+
details: { background: true, ...about },
|
|
807
|
+
};
|
|
808
|
+
};
|
|
663
809
|
pi.registerTool({
|
|
664
810
|
name: "cultivate",
|
|
665
811
|
label: "Cultivate",
|
|
@@ -674,6 +820,7 @@ function turriExtension(api, session, link) {
|
|
|
674
820
|
// pi lists .agents/skills among its own skills too: read and followed by hand, a skill's gates would be skipped.
|
|
675
821
|
"A skill cultivate lists may also be among your own skills, from the project's .agents/skills: run it with cultivate all the same, never by reading its SKILL.md and following it.",
|
|
676
822
|
"cultivate returns the reviewed artifact and a trace of every gate decision. Report the artifact to the user; do not rewrite it.",
|
|
823
|
+
"A skill that runs in the background returns at once: go on with the turn, without waiting or telling the person to wait. Its end comes back to this chat by itself - a line when it did its work, a message to act on when it did not.",
|
|
677
824
|
"If cultivate fails, say so and stop. Never carry out the skill's work yourself in the conversation - the gates and the judges are the point of running it, and an unreviewed hand-run silently drops them.",
|
|
678
825
|
"Always pass `context` to cultivate, summarising what the user has already told you.",
|
|
679
826
|
],
|
|
@@ -694,6 +841,10 @@ function turriExtension(api, session, link) {
|
|
|
694
841
|
fresh: Type.Optional(Type.Boolean({
|
|
695
842
|
description: "Start a new run even though an interrupted one exists here, discarding it.",
|
|
696
843
|
})),
|
|
844
|
+
plainFile: Type.Optional(Type.String({
|
|
845
|
+
description: "The module the skill's work is for - its .plain file, e.g. backend.plain: a testing script is written for that module's stack, " +
|
|
846
|
+
"on its coding model. Omitted: the project's.",
|
|
847
|
+
})),
|
|
697
848
|
}),
|
|
698
849
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
699
850
|
const skill = skills.find((s) => s.name === params.skill);
|
|
@@ -705,6 +856,23 @@ function turriExtension(api, session, link) {
|
|
|
705
856
|
}
|
|
706
857
|
if (!gate.mayStart(skill))
|
|
707
858
|
return { content: [{ type: "text", text: startedByCommandOnly(skill.name) }], details: { notStarted: true } };
|
|
859
|
+
// The conformance testing script is for conformance tests: none is written while they are off.
|
|
860
|
+
if (scriptKindOfSkill(skill.name) === "conformance" && !conformanceTestsOnIn(cwd)) {
|
|
861
|
+
return {
|
|
862
|
+
content: [{ type: "text", text: `${skill.name} writes the conformance testing script, and conformance tests are off for this project (conformance-tests: false, or unset). Turn them on first - with ${TURN_ON_TOOL}, once the person asked for them or agreed - then run it. Nothing was started.` }],
|
|
863
|
+
details: { notStarted: true },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const plainFile = params.plainFile !== undefined && params.plainFile.trim() !== "" ? resolve(cwd, params.plainFile.trim()) : undefined;
|
|
867
|
+
if (plainFile !== undefined && !existsSync(plainFile)) {
|
|
868
|
+
return { content: [{ type: "text", text: `There is no ${params.plainFile} here. Nothing was started.` }], details: { notStarted: true } };
|
|
869
|
+
}
|
|
870
|
+
// A skill that runs in the background, in a chat - where its end can come back to: started, and the turn goes on.
|
|
871
|
+
if (skill.background && !shouldWait(ctx.mode, undefined)) {
|
|
872
|
+
gate.started(skill.name);
|
|
873
|
+
const started = startInBackground(skill, params, plainFile, ctx);
|
|
874
|
+
return { content: [{ type: "text", text: started.text }], details: started.details };
|
|
875
|
+
}
|
|
708
876
|
// A new run clears the run folder. An interrupted one is not thrown away by accident:
|
|
709
877
|
// whether to pick it up or start over is the user's call.
|
|
710
878
|
const runDir = skillRunDir(cwd, skill.name);
|
|
@@ -846,13 +1014,13 @@ function turriExtension(api, session, link) {
|
|
|
846
1014
|
push(` A: ${answer}`);
|
|
847
1015
|
recordExchange(pi, { skill: skill.name, question, answer, ...(said !== undefined && said.trim() !== "" ? { said: said.trim() } : {}) });
|
|
848
1016
|
}, (question) => askOnTheSide(pi, ctx, cwd, jobs, question), dialogs),
|
|
849
|
-
model: conversationModel(ctx),
|
|
1017
|
+
model: skill.model === "coding" ? jobModel(ctx, "spec_changes", undefined, plainFile) : conversationModel(ctx),
|
|
850
1018
|
// Renders queued by a step land in the same registry as everything else, so
|
|
851
1019
|
// pulses sees them without knowing a skill started them.
|
|
852
1020
|
// Asked per job as it is queued, so a model chosen mid-interview applies from then on.
|
|
853
1021
|
renderQueue: workLoopFor(jobs, cwd, ctx),
|
|
854
1022
|
// The person answers the skill's agent while it runs, so it can take "render these with ..." too.
|
|
855
|
-
//
|
|
1023
|
+
// parse too: a skill parses what it wrote with the same check the conversation has.
|
|
856
1024
|
sharedTools: [setRenderModelTool(pi, ctx), checkPlainTool(cwd), ...createWebTools()],
|
|
857
1025
|
// The conversation's own regenerate, drift, halt and conform, on its loop: a skill waits for nothing, and regenerates on the model chosen.
|
|
858
1026
|
loopTools: [
|
|
@@ -911,9 +1079,14 @@ function turriExtension(api, session, link) {
|
|
|
911
1079
|
? "It changed no file in the project - whatever it said it wrote is not there. Say so to the user; do not report the work as done."
|
|
912
1080
|
: "It only reads: what it found is the artifact below.";
|
|
913
1081
|
// A skill that edits the specs with the file tools builds nothing: the change is the conversation's to render.
|
|
1082
|
+
// A test plain file is its module's: the module is what regenerate is called for, its scenarios' tests brought up with it.
|
|
914
1083
|
const specsChanged = changes.filter((path) => path.endsWith(".plain"));
|
|
1084
|
+
const named = specsChanged.map((path) => {
|
|
1085
|
+
const owner = moduleOfTestPlainFile(resolve(cwd, path));
|
|
1086
|
+
return owner === undefined ? path : `${path} (a test plain file of ${owner.plainFile !== undefined ? relative(cwd, owner.plainFile) : owner.module})`;
|
|
1087
|
+
});
|
|
915
1088
|
const unrendered = specsChanged.length > 0 && !skill.allowedTools.includes("change_plain")
|
|
916
|
-
? `${
|
|
1089
|
+
? `${named.join(", ")} changed, and nothing is regenerated from that yet: call regenerate for ${named.length === 1 ? "its module" : "their modules"}, and the code and its conformance tests are brought up to the spec.`
|
|
917
1090
|
: "";
|
|
918
1091
|
const gated = skill.steps.some((step) => step.gate !== undefined);
|
|
919
1092
|
const first = skill.allowedTools.includes("change_plain") ? firstRegeneration(jobs.list(), began) : {};
|
|
@@ -955,7 +1128,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
955
1128
|
// by the planner: one functional spec at a time, a few together or the whole module in one, as it judges. What
|
|
956
1129
|
// follows the module is guidance for that plan, in the person's words.
|
|
957
1130
|
pi.registerCommand("regenerate", {
|
|
958
|
-
description: "Regenerate a module from its ***plain as it is now, in pulses the planner cuts - named
|
|
1131
|
+
description: "Regenerate a module from its ***plain as it is now, in pulses the planner cuts - named functional specs though they did not change, or all of it from scratch, its whole chain too when asked: /regenerate [module | file.plain] [module:func-id ...] [from scratch [whole chain]] [how to cut the work]",
|
|
959
1132
|
handler: async (args, ctx) => {
|
|
960
1133
|
const command = `/regenerate ${args.trim()}`.trim();
|
|
961
1134
|
await echoCommand(pi, command);
|
|
@@ -967,11 +1140,11 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
967
1140
|
say(`There is no ${first} here.`, "error");
|
|
968
1141
|
return;
|
|
969
1142
|
}
|
|
970
|
-
//
|
|
1143
|
+
// Functional specs named first, each as module:func-id, as on the command line; what follows them is guidance.
|
|
971
1144
|
const words = rest;
|
|
972
1145
|
const again = [];
|
|
973
|
-
while (words.length > 0 && /^
|
|
974
|
-
again.push(words.shift().replace(
|
|
1146
|
+
while (words.length > 0 && /^[^\s:]+:[a-z0-9-]+,?$/.test(words[0]))
|
|
1147
|
+
again.push(words.shift().replace(/,$/, ""));
|
|
975
1148
|
const guidance = words.join(" ").trim();
|
|
976
1149
|
let plainFile = named;
|
|
977
1150
|
if (plainFile === undefined) {
|
|
@@ -992,7 +1165,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
992
1165
|
}
|
|
993
1166
|
const check = checkPlainFile(plainFile);
|
|
994
1167
|
if (!check.ok) {
|
|
995
|
-
say(`${relative(cwd, plainFile) || plainFile} does not parse, so nothing can be regenerated from it yet
|
|
1168
|
+
say(`${relative(cwd, plainFile) || plainFile} does not parse, so nothing can be regenerated from it yet:\n${formatCheckResults([check])}`, "error");
|
|
996
1169
|
return;
|
|
997
1170
|
}
|
|
998
1171
|
// From scratch - started over, its code deleted - asked in the person's words: started at once, as asked.
|
|
@@ -1001,7 +1174,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
1001
1174
|
const chain = scratch && asksWholeChain(guidance);
|
|
1002
1175
|
const module = basename(plainFile).replace(/\.plain$/, "");
|
|
1003
1176
|
if (scratch && again.length > 0) {
|
|
1004
|
-
say("Name
|
|
1177
|
+
say("Name functional specs or ask for it from scratch, not both: from scratch regenerates every one of them.", "warning");
|
|
1005
1178
|
return;
|
|
1006
1179
|
}
|
|
1007
1180
|
const loop = workLoopFor(jobs, cwd, ctx);
|
|
@@ -1012,7 +1185,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
1012
1185
|
say(scratch
|
|
1013
1186
|
? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""}, then planning all of it again…`
|
|
1014
1187
|
: again.length > 0
|
|
1015
|
-
? `Planning ${
|
|
1188
|
+
? `Planning ${again.join(", ")} again…`
|
|
1016
1189
|
: `Planning ${module}'s pulses…`, "info");
|
|
1017
1190
|
await nextFrame();
|
|
1018
1191
|
// Planned in the background: pi reads what is typed next only once a command returns, and planning takes the
|
|
@@ -1029,7 +1202,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
1029
1202
|
// Nothing to do: say how to build it again anyway.
|
|
1030
1203
|
const upToDate = statuses.length > 0 && statuses.every(isUpToDate);
|
|
1031
1204
|
const hint = upToDate
|
|
1032
|
-
? `\nTo regenerate parts of it anyway, name their
|
|
1205
|
+
? `\nTo regenerate parts of it anyway, name their functional specs - /regenerate ${basename(target)} ${module}:<func-id> - or start over with /regenerate ${basename(target)} from scratch.`
|
|
1033
1206
|
: "";
|
|
1034
1207
|
// Pulses started: the agent says so, and that the person need not wait - a plan alone leaves the chat silent.
|
|
1035
1208
|
shownAfterCommand(pi, ctx, command, describeModules(statuses) + hint, "info", true, upToDate ? undefined : COMMAND_STARTED_REACTION);
|
|
@@ -1109,7 +1282,8 @@ export function moduleNamedFirst(cwd, words) {
|
|
|
1109
1282
|
function plainFileNamed(cwd, word) {
|
|
1110
1283
|
for (const candidate of [word, `${word}.plain`]) {
|
|
1111
1284
|
const path = resolve(cwd, candidate);
|
|
1112
|
-
|
|
1285
|
+
// A test plain file names no module: it is refused where the module is taken.
|
|
1286
|
+
if (path.endsWith(".plain") && existsSync(path) && testPlainFileRefusal(path) === undefined)
|
|
1113
1287
|
return path;
|
|
1114
1288
|
}
|
|
1115
1289
|
return undefined;
|
|
@@ -1135,31 +1309,27 @@ function conformTool(options) {
|
|
|
1135
1309
|
return defineTool({
|
|
1136
1310
|
name: "conform",
|
|
1137
1311
|
label: "Conform",
|
|
1138
|
-
description: "Write and run the black-box conformance tests of a module's functional specs - the ones named, or every one
|
|
1139
|
-
"the code as it is built, in a pulse of their own - every functional spec tested in one, as far as a size
|
|
1140
|
-
"lets, or one a functional spec when the user asks for that: each
|
|
1141
|
-
"
|
|
1142
|
-
"
|
|
1143
|
-
"
|
|
1144
|
-
"
|
|
1145
|
-
"
|
|
1146
|
-
"
|
|
1147
|
-
|
|
1312
|
+
description: "Write and run the black-box conformance tests of a module's functional specs - the ones named, or every one with " +
|
|
1313
|
+
"scenarios - on the code as it is built, in a pulse of their own - every functional spec tested in one, as far as a size " +
|
|
1314
|
+
"guardrail lets, or one a functional spec when the user asks for that: each implements exactly the functional spec's " +
|
|
1315
|
+
"conformance and acceptance scenarios, at least one test each, by the spec's test reqs, runs them with the whole suite " +
|
|
1316
|
+
"the module inherits, and, where one fails, fixes the test or the code, whichever is wrong. What they are written for is " +
|
|
1317
|
+
"recorded apart from the code. The work loop starts these pulses by itself for every functional spec with scenarios whose " +
|
|
1318
|
+
"tests are behind, once the module's code is built; conform starts them now, or for the functional specs named - those alone: the module's others stay behind until conform names none, or the module is regenerated. A " +
|
|
1319
|
+
"functional spec with no scenarios - no test plain file - is refused: there is nothing to test until scenarios " +
|
|
1320
|
+
"are written. So is one the spec does not have, and a module with scenarios that skipped a step of adding tests - " +
|
|
1321
|
+
"no test reqs, or no conformance testing script for its stack: the refusal names the next step.",
|
|
1322
|
+
promptSnippet: "conform: write and run the conformance tests of functional specs with scenarios",
|
|
1148
1323
|
promptGuidelines: [
|
|
1149
|
-
"When the user asks for conformance tests
|
|
1150
|
-
"
|
|
1151
|
-
"What the user asks for comes first: for a module already regenerated, while a module built on it is being regenerated, conform stops that pulse, deletes the codebase built on it, writes the tests, and the module above is regenerated from it after - say so. For a module not regenerated yet
|
|
1324
|
+
"What is tested is the scenarios: a functional spec's conformance and acceptance scenarios, in its test plain file, tests/<module>/<func-id>.plain. When the user asks for conformance tests, decide in a sentence or two which scenarios to write, and write them with change_plain - a test plain file's ***conformance tests*** and ***acceptance tests***, each scenario a # <scenario-id> heading and its text. Every scenario written is tested once the code is built: the loop starts the conformance pulse by itself. Never edit config.turri.yaml for it, and never regenerate a functionality to get them.",
|
|
1325
|
+
"Call conform when the user asks for the conformance tests to be written or run now, for a module or some of its functional specs with scenarios.",
|
|
1326
|
+
"What the user asks for comes first: for a module already regenerated, while a module built on it is being regenerated, conform stops that pulse, deletes the codebase built on it, writes the tests, and the module above is regenerated from it after - say so. For a module not regenerated yet, its tests are written once its code is built, before anything is built on it.",
|
|
1152
1327
|
"Conformance tests go in one pulse for all the functional specs tested. Set one_by_one only when the user asks for a pulse a functional spec - one at a time, separately, or the like.",
|
|
1153
|
-
"When the user asks for the conformance tests in pulses of their own grouping - 1-3 and 4-6 - pass those groups as groups, in one call: each group is a pulse of its own, never sharing one with another. Without groups, every functional spec named goes in one pulse.",
|
|
1154
|
-
"A conformance pulse that did not start because its code is not built: tell the user which functional specs it waits for, and which code pulse builds them.",
|
|
1155
1328
|
],
|
|
1156
1329
|
parameters: Type.Object({
|
|
1157
1330
|
plain_file: Type.String({ description: "A .plain file of the module" }),
|
|
1158
1331
|
working_dir: WORKING_DIR_PARAM,
|
|
1159
|
-
|
|
1160
|
-
groups: Type.Optional(Type.Array(Type.Array(Type.String()), {
|
|
1161
|
-
description: "Only when the user asks for them in pulses of their own grouping: the functional specs by id, a list a pulse - [[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]. Each is tested in a pulse of its own; they are added to those chosen, as frids are",
|
|
1162
|
-
})),
|
|
1332
|
+
funcIds: Type.Optional(Type.Array(Type.String(), { description: "The functional specs to test, each as module:func-id (todo:add-task) - each with scenarios. Omitted: every one with scenarios" })),
|
|
1163
1333
|
one_by_one: Type.Optional(Type.Boolean({ description: "Only when the user asks for it: a pulse a functional spec. Omitted: every functional spec tested in one pulse, as far as the size guardrail lets" })),
|
|
1164
1334
|
stack: STACK_PARAM,
|
|
1165
1335
|
model: MODEL_PARAM,
|
|
@@ -1168,12 +1338,12 @@ function conformTool(options) {
|
|
|
1168
1338
|
async execute(_id, given, _signal, _onUpdate, ctx) {
|
|
1169
1339
|
const params = fromWorkingDir(cwd, given);
|
|
1170
1340
|
const plainFile = resolve(params.base, params.plain_file);
|
|
1341
|
+
refuseTestPlainFile(plainFile);
|
|
1171
1342
|
return withCallStack(plainFile, params.stack, async () => {
|
|
1172
1343
|
const model = options.model(ctx, plainFile, params.model);
|
|
1173
1344
|
const loop = options.loop(ctx);
|
|
1174
1345
|
const statuses = await loop.conformance(plainFile, {
|
|
1175
|
-
...(params.
|
|
1176
|
-
...(params.groups !== undefined ? { groups: params.groups } : {}),
|
|
1346
|
+
...(params.funcIds !== undefined ? { funcIds: params.funcIds } : {}),
|
|
1177
1347
|
...(model !== undefined ? { model } : {}),
|
|
1178
1348
|
...(params.one_by_one === true ? { separately: true } : {}),
|
|
1179
1349
|
});
|
|
@@ -1201,6 +1371,7 @@ function registerGlimpse(pi, cwd, jobs) {
|
|
|
1201
1371
|
async execute(_id, given, _signal, _onUpdate, ctx) {
|
|
1202
1372
|
const params = fromWorkingDir(cwd, given);
|
|
1203
1373
|
const plainFile = resolve(params.base, params.plain_file);
|
|
1374
|
+
refuseTestPlainFile(plainFile);
|
|
1204
1375
|
return withCallStack(plainFile, params.stack, async () => {
|
|
1205
1376
|
const running = new Set(workLoopFor(jobs, cwd, ctx).status().filter((status) => status.running !== undefined).map((status) => status.module));
|
|
1206
1377
|
const planner = chatModelOf(ctx);
|
|
@@ -1363,15 +1534,9 @@ export function projectConfigBlock(root) {
|
|
|
1363
1534
|
heading,
|
|
1364
1535
|
...[
|
|
1365
1536
|
config.module !== undefined && of !== "stack" ? `- module: ${config.module}${where} - the module to build when the user does not name one.` : undefined,
|
|
1366
|
-
|
|
1367
|
-
? "- conformance
|
|
1368
|
-
:
|
|
1369
|
-
? "- conformance-tests: chosen - conformance tests are on for the functional specs chosen: those with acceptance tests always, those the chooser picks as each code pulse ends from what it regenerated, and those the user asks for with conform. A conformance pulse starts once the module's code is built, before anything is built on it. Once chosen, a functional spec keeps them."
|
|
1370
|
-
: config.conformanceTests === "module"
|
|
1371
|
-
? "- conformance-tests: module - conformance tests are on for the functional specs chosen: those with acceptance tests always, those the chooser picks - and plans into pulses - once the whole module is regenerated, and those the user asks for with conform. They run before anything is built on the module. Once chosen, a functional spec keeps them."
|
|
1372
|
-
: config.conformanceTests === true
|
|
1373
|
-
? "- conformance-tests: all - conformance tests are on, for every functional spec of every module that has test reqs."
|
|
1374
|
-
: "- conformance-tests: not set - conformance tests are OFF, as with false: no new ones are written, unless the user asks for them in this conversation; the ones there are are kept up to date - a functional spec that has them and is regenerated gets a conformance pulse for it alone once its code lands.",
|
|
1537
|
+
of !== "stack"
|
|
1538
|
+
? "- conformance tests: written for every functional spec with scenarios - the conformance and acceptance scenarios of its test plain file - once the module's code is built, before anything is built on it. A functional spec with an empty test plain file is not tested. To have one tested, write its scenarios with change_plain."
|
|
1539
|
+
: undefined,
|
|
1375
1540
|
config.review === true
|
|
1376
1541
|
? "- review: true - the code and its tests are reviewed once they pass: two reviews at most, the arbiter weighing the second."
|
|
1377
1542
|
: `- review: ${config.review === false ? "false" : "not set, so off"} - the code and its tests are not reviewed: a pulse lands once its tests pass.`,
|
|
@@ -1444,6 +1609,27 @@ function chatModelOf(ctx) {
|
|
|
1444
1609
|
}
|
|
1445
1610
|
}
|
|
1446
1611
|
let lastChatModel;
|
|
1612
|
+
/**
|
|
1613
|
+
* Conformance tests turned on, for the session: `conformance-tests: true` written where the module's value comes from
|
|
1614
|
+
* (conformance-switch.ts), and every open chat's tools switched to match - what the chat's turn_on_conformance_tests and a
|
|
1615
|
+
* host's turnOnConformanceTests both do. `plainFile`, absolute or from the project, picks the module whose stack's file
|
|
1616
|
+
* is written when that names the key. Returns the file written, from the project. Starts no conformance pulse.
|
|
1617
|
+
*/
|
|
1618
|
+
export function turnOnConformance(core, plainFile) {
|
|
1619
|
+
const file = turnOnConformanceTests(core.cwd, plainFile !== undefined && plainFile.trim() !== "" ? resolve(core.cwd, plainFile) : undefined);
|
|
1620
|
+
for (const chat of core.chats()) {
|
|
1621
|
+
const live = core.session(chat.id);
|
|
1622
|
+
try {
|
|
1623
|
+
(live !== undefined ? core.chatOfSession(live) : undefined)?.instance?.conformanceSwitched?.();
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
/* a chat let go: its next turn sets its tools */
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return file;
|
|
1630
|
+
}
|
|
1631
|
+
/** The skills running in the background, by project and skill - one at a time each, whichever chat started it - with what stops them. */
|
|
1632
|
+
const backgroundSkillRuns = new Map();
|
|
1447
1633
|
/** The models chosen in this session, by kind of work - for every chat. Saved in the chat they were chosen in, so reopening it keeps them. */
|
|
1448
1634
|
const chosenModels = {};
|
|
1449
1635
|
/** When they were last chosen: a chat reopened with older choices of its own leaves them be. */
|
|
@@ -1459,6 +1645,17 @@ const MODEL_CHOICE_ENTRY = "turri_render_models";
|
|
|
1459
1645
|
* of work, or the project's - `model:` in config.turri.yaml - or the conversation's.
|
|
1460
1646
|
*/
|
|
1461
1647
|
function jobModel(ctx, work, named, plainFile) {
|
|
1648
|
+
const model = jobModelOf(ctx, work, named, plainFile);
|
|
1649
|
+
// The AWS region worked out again, with every model a job ran on this session: the config's may have changed since,
|
|
1650
|
+
// and one chosen in the session counts as the config's do - two needing two regions are said, not flipped between.
|
|
1651
|
+
if (model !== undefined)
|
|
1652
|
+
jobModels.add(model);
|
|
1653
|
+
applyBedrockRegion(plainFile !== undefined ? projectDirOf(plainFile) : process.cwd(), { also: [...jobModels] });
|
|
1654
|
+
return model;
|
|
1655
|
+
}
|
|
1656
|
+
/** Every model a job ran on in this session. */
|
|
1657
|
+
const jobModels = new Set();
|
|
1658
|
+
function jobModelOf(ctx, work, named, plainFile) {
|
|
1462
1659
|
if (named !== undefined && named.trim() !== "") {
|
|
1463
1660
|
const resolved = resolveModel(named, ctx.modelRegistry);
|
|
1464
1661
|
if (!resolved.ok)
|
|
@@ -1613,6 +1810,12 @@ const workLoops = new WeakMap();
|
|
|
1613
1810
|
const steeringStore = createSteeringStore();
|
|
1614
1811
|
/** What release has shipped in a conversation, by its jobs: shared by the chat's release and a pulse's own. */
|
|
1615
1812
|
const deployStates = new WeakMap();
|
|
1813
|
+
/** A session's test advisor, by its jobs - only a chat's or a host's has one. */
|
|
1814
|
+
const advisors = new WeakMap();
|
|
1815
|
+
/** The session's test advisor, when it has one: the chat's and a host's - never pi's print, json and rpc modes'. */
|
|
1816
|
+
export function sessionAdvisor(session) {
|
|
1817
|
+
return advisors.get(session.jobs);
|
|
1818
|
+
}
|
|
1616
1819
|
function workLoopFor(jobs, cwd, ctx) {
|
|
1617
1820
|
let shared = workLoops.get(jobs);
|
|
1618
1821
|
if (shared === undefined) {
|
|
@@ -1623,11 +1826,10 @@ function workLoopFor(jobs, cwd, ctx) {
|
|
|
1623
1826
|
modelFor: (plainFile) => modelFor(chosenModels, "spec_changes", projectModel(from.ctx, plainFile) ?? chatModelOf(from.ctx)),
|
|
1624
1827
|
// The planner reasons about the whole of the work: it runs on the chat's model.
|
|
1625
1828
|
plannerModel: () => chatModelOf(from.ctx),
|
|
1626
|
-
// Under conformance-tests: chosen or module, the chooser decides as a pulse ends - on the chat's model - so the choice
|
|
1627
|
-
// is made before anything is built on the module; the pulse's report says what it chose, and why.
|
|
1628
|
-
onChoose: conformanceChooser({ model: () => chatModelOf(from.ctx) }),
|
|
1629
1829
|
// How the work is to be planned: the conversation's steering, and AGENTS.md's - read afresh each plan.
|
|
1630
1830
|
steering: (plainFile) => steeringFor(plainFile, cwd, steeringStore),
|
|
1831
|
+
// What the loop looks at, the test advisor sees - in a session that has one.
|
|
1832
|
+
onSpec: (seen) => advisors.get(jobs)?.seen(seen),
|
|
1631
1833
|
// Under release-after-pulse, a pulse releases what it landed itself, before the next starts - on the model that
|
|
1632
1834
|
// regenerated it: a release follows steps, and a small model thinking little does that well.
|
|
1633
1835
|
release: ({ plainFile, moduleName, title, onDelta }) => {
|
|
@@ -1642,6 +1844,7 @@ function workLoopFor(jobs, cwd, ctx) {
|
|
|
1642
1844
|
});
|
|
1643
1845
|
},
|
|
1644
1846
|
});
|
|
1847
|
+
bindAdvisor(loop, () => advisors.get(jobs));
|
|
1645
1848
|
shared = { loop, from };
|
|
1646
1849
|
workLoops.set(jobs, shared);
|
|
1647
1850
|
}
|
|
@@ -1905,7 +2108,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1905
2108
|
// start changed here, as it runs.
|
|
1906
2109
|
registerTurriConfig(pi, {
|
|
1907
2110
|
cwd,
|
|
1908
|
-
rules:
|
|
2111
|
+
rules: ruleNames,
|
|
1909
2112
|
chatSpec: () => modelSpec(projectModels(readProjectConfig(cwd)).harness),
|
|
1910
2113
|
live: {
|
|
1911
2114
|
chatModel: async (spec, ctx) => {
|
|
@@ -2132,7 +2335,7 @@ function sidePanelOf(jobs, cwd) {
|
|
|
2132
2335
|
warning: (text) => theme.fg("warning", text),
|
|
2133
2336
|
added: (text) => theme.fg("toolDiffAdded", text),
|
|
2134
2337
|
removed: (text) => theme.fg("toolDiffRemoved", text),
|
|
2135
|
-
// A spec diff's ***plain highlighted
|
|
2338
|
+
// A spec diff's ***plain highlighted by turri's grammar, and its added and removed lines filled.
|
|
2136
2339
|
diff: plainPaint(theme),
|
|
2137
2340
|
// What conformance tests show does what its spec says: neon.
|
|
2138
2341
|
conforms: brandNeon(theme),
|
|
@@ -2319,7 +2522,7 @@ function jobLines(all) {
|
|
|
2319
2522
|
.join("\n");
|
|
2320
2523
|
}
|
|
2321
2524
|
/**
|
|
2322
|
-
*
|
|
2525
|
+
* The ***plain parsed - its syntax and structure - regenerating nothing.
|
|
2323
2526
|
*
|
|
2324
2527
|
* This is the check that used to mean a renderer dry-run. It runs this repo's own parser,
|
|
2325
2528
|
* so it needs no external tool, no API key and no credits - and it is the only honest way
|
|
@@ -2484,32 +2687,6 @@ function registerDeploy(pi, cwd, jobs, state) {
|
|
|
2484
2687
|
await stopDeployed();
|
|
2485
2688
|
});
|
|
2486
2689
|
}
|
|
2487
|
-
/** validate, for the conversation and for the skills that write specs alike. */
|
|
2488
|
-
function checkPlainTool(cwd) {
|
|
2489
|
-
return defineTool({
|
|
2490
|
-
name: "validate",
|
|
2491
|
-
label: "Validate spec",
|
|
2492
|
-
description: "Validate one or more .plain specifications without rendering: parses each file, resolves its " +
|
|
2493
|
-
"imports and required modules, and reports every syntax, concept and structure error with the " +
|
|
2494
|
-
"line it is on. Pass a directory to check every .plain file in it. Fast, free, and offline.",
|
|
2495
|
-
promptSnippet: "validate a .plain spec, regenerating nothing",
|
|
2496
|
-
promptGuidelines: [
|
|
2497
|
-
"Run validate after editing a .plain file, and before telling the user a spec is valid or ready. Never shell out to another renderer to check one.",
|
|
2498
|
-
],
|
|
2499
|
-
parameters: Type.Object({
|
|
2500
|
-
target: Type.String({ description: "A .plain file, or a directory holding .plain files" }),
|
|
2501
|
-
template_dir: Type.Optional(Type.Array(Type.String(), { description: "Extra template directories" })),
|
|
2502
|
-
}),
|
|
2503
|
-
async execute(_id, params) {
|
|
2504
|
-
const results = checkPlainTargets(resolve(cwd, params.target), (params.template_dir ?? []).map((dir) => resolve(cwd, dir)));
|
|
2505
|
-
const failed = results.filter((result) => !result.ok).length;
|
|
2506
|
-
const text = results.length === 0
|
|
2507
|
-
? `No .plain files found in ${params.target}.`
|
|
2508
|
-
: `${formatCheckResults(results)}\n\n${failed === 0 ? "All specs valid." : `${failed} of ${results.length} failed.`}`;
|
|
2509
|
-
return { content: [{ type: "text", text }], details: { failed, checked: results.length } };
|
|
2510
|
-
},
|
|
2511
|
-
});
|
|
2512
|
-
}
|
|
2513
2690
|
const MILESTONE_ENTRY = "turri_milestone";
|
|
2514
2691
|
const JOB_ENTRY = "turri_job";
|
|
2515
2692
|
const JOBS_LIST_ENTRY = "turri_jobs";
|
|
@@ -2551,7 +2728,7 @@ function specConflictTask(conflict) {
|
|
|
2551
2728
|
describeSpecConflict(conflict),
|
|
2552
2729
|
"",
|
|
2553
2730
|
`The render stopped on it: the conformance run's judge found them conflicting, ${conflict.reasons.length} times -`,
|
|
2554
|
-
`functional spec ${one.
|
|
2731
|
+
`functional spec ${one.module}:${one.funcId} (${one.plainFile}) and functional spec ${two.module}:${two.funcId} (${two.plainFile}). Now:`,
|
|
2555
2732
|
"1. Run the analyze-2-func-specs skill (cultivate) on these two functional specs, to confirm they conflict.",
|
|
2556
2733
|
"2. If they do, run the resolve-spec-conflict skill on the pair, to work out what could be changed - but change nothing yet.",
|
|
2557
2734
|
"3. Tell the user exactly what the conflict is - both specs as they read, and the behaviour that cannot satisfy both -",
|
|
@@ -2724,7 +2901,7 @@ function registerBranding(pi, skills) {
|
|
|
2724
2901
|
const lines = [
|
|
2725
2902
|
`${brandPink(theme)("turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2726
2903
|
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2727
|
-
theme.fg("text", "the first coding agent with the power of regenerative software")
|
|
2904
|
+
`${theme.fg("text", "the first coding agent with the power of ")}${brandPink(theme)("regenerative software")}`,
|
|
2728
2905
|
// Where the settings are changed, without leaving the chat.
|
|
2729
2906
|
theme.fg("muted", "configure with /turri-config"),
|
|
2730
2907
|
// Only what is wrong: skills that did not load, and agents' commands less fenced than they should be.
|