@turri-dev/cli 0.0.0-stage → 0.1.1
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/.pi/themes/turri-dark.json +80 -0
- package/.pi/themes/turri-light.json +82 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +252 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
- package/bin/turri.mjs +29 -0
- package/dist/ask.js +149 -0
- package/dist/brand.js +50 -0
- package/dist/btw.js +174 -0
- package/dist/chat-models.js +37 -0
- package/dist/chat-skills.js +18 -0
- package/dist/cli.js +178 -0
- package/dist/config-command.js +197 -0
- package/dist/config-edit.js +367 -0
- package/dist/config.js +489 -0
- package/dist/conform-cli.js +29 -0
- package/dist/cultivate-cli.js +110 -0
- package/dist/drift-cli.js +25 -0
- package/dist/environment.js +70 -0
- package/dist/extension.js +2372 -0
- package/dist/genome.js +301 -0
- package/dist/graph.js +246 -0
- package/dist/identity.js +35 -0
- package/dist/init-config.js +511 -0
- package/dist/judge.js +62 -0
- package/dist/lineage-cli.js +13 -0
- package/dist/loop-cli.js +132 -0
- package/dist/loop-tools.js +225 -0
- package/dist/mature-tools.js +193 -0
- package/dist/model-check.js +117 -0
- package/dist/module-report.js +9 -0
- package/dist/pi-launch.js +31 -0
- package/dist/pi.js +813 -0
- package/dist/plain/check.js +76 -0
- package/dist/plain/concepts.js +213 -0
- package/dist/plain/errors.js +43 -0
- package/dist/plain/files.js +57 -0
- package/dist/plain/highlight.js +195 -0
- package/dist/plain/index.js +15 -0
- package/dist/plain/liquid.js +129 -0
- package/dist/plain/markdown.js +208 -0
- package/dist/plain/parser.js +752 -0
- package/dist/plain/plain.tmLanguage.json +124 -0
- package/dist/plain/spec.js +291 -0
- package/dist/plain/stdlib.js +44 -0
- package/dist/question-dialog.js +125 -0
- package/dist/questioning.js +109 -0
- package/dist/quiet.js +125 -0
- package/dist/regenerate/activity.js +187 -0
- package/dist/regenerate/agent-env.js +190 -0
- package/dist/regenerate/agent-shell.js +114 -0
- package/dist/regenerate/arbiter.js +133 -0
- package/dist/regenerate/audit.js +203 -0
- package/dist/regenerate/cases.js +83 -0
- package/dist/regenerate/change-view.js +102 -0
- package/dist/regenerate/choose-conformance.js +117 -0
- package/dist/regenerate/chunk-plan.js +625 -0
- package/dist/regenerate/codebase.js +75 -0
- package/dist/regenerate/command-check.js +312 -0
- package/dist/regenerate/compaction.js +20 -0
- package/dist/regenerate/confine.js +310 -0
- package/dist/regenerate/conform.js +133 -0
- package/dist/regenerate/conformance-agents.js +955 -0
- package/dist/regenerate/conformance.js +936 -0
- package/dist/regenerate/conversation-writes.js +100 -0
- package/dist/regenerate/credentials.js +88 -0
- package/dist/regenerate/debug-log.js +228 -0
- package/dist/regenerate/debugger.js +90 -0
- package/dist/regenerate/delivery.js +19 -0
- package/dist/regenerate/diagnose-job.js +40 -0
- package/dist/regenerate/diagnose.js +81 -0
- package/dist/regenerate/diff-view.js +240 -0
- package/dist/regenerate/drift.js +194 -0
- package/dist/regenerate/engine.js +118 -0
- package/dist/regenerate/environment-error.js +220 -0
- package/dist/regenerate/job-log.js +102 -0
- package/dist/regenerate/job-panel.js +336 -0
- package/dist/regenerate/jobs-tree.js +300 -0
- package/dist/regenerate/jobs.js +427 -0
- package/dist/regenerate/lanes.js +32 -0
- package/dist/regenerate/lineage.js +384 -0
- package/dist/regenerate/loop.js +1435 -0
- package/dist/regenerate/masking.js +95 -0
- package/dist/regenerate/notes.js +397 -0
- package/dist/regenerate/os-sandbox.js +243 -0
- package/dist/regenerate/paths.js +165 -0
- package/dist/regenerate/plain-palette.js +83 -0
- package/dist/regenerate/prompt.js +704 -0
- package/dist/regenerate/records.js +294 -0
- package/dist/regenerate/regenerate.js +598 -0
- package/dist/regenerate/regression.js +181 -0
- package/dist/regenerate/release.js +262 -0
- package/dist/regenerate/requires.js +241 -0
- package/dist/regenerate/resources.js +164 -0
- package/dist/regenerate/self-check.js +111 -0
- package/dist/regenerate/services.js +155 -0
- package/dist/regenerate/shell.js +319 -0
- package/dist/regenerate/side-panel.js +278 -0
- package/dist/regenerate/spec-map-view.js +574 -0
- package/dist/regenerate/spec-map.js +561 -0
- package/dist/regenerate/spec-steps.js +527 -0
- package/dist/regenerate/state.js +10 -0
- package/dist/regenerate/steer.js +91 -0
- package/dist/regenerate/steering.js +119 -0
- package/dist/regenerate/stop-report.js +161 -0
- package/dist/regenerate/submit-work.js +71 -0
- package/dist/regenerate/test-kinds.js +124 -0
- package/dist/regenerate/test-scripts.js +695 -0
- package/dist/regenerate/testing.js +406 -0
- package/dist/regenerate/transcript-view.js +1 -0
- package/dist/regenerate/unit-gate.js +20 -0
- package/dist/regenerate/unit-review.js +347 -0
- package/dist/regenerate/web.js +256 -0
- package/dist/regenerate/workspace.js +99 -0
- package/dist/regenerate-cli.js +68 -0
- package/dist/release-agent.js +114 -0
- package/dist/release-cli.js +131 -0
- package/dist/release-tools.js +148 -0
- package/dist/review/live.js +165 -0
- package/dist/rules.js +47 -0
- package/dist/self-invocation.js +110 -0
- package/dist/setup.js +245 -0
- package/dist/skill-progress.js +78 -0
- package/dist/skill.js +149 -0
- package/dist/skills.js +74 -0
- package/dist/spec-diff-cli.js +37 -0
- package/dist/spec-tools.js +155 -0
- package/dist/stack.js +158 -0
- package/dist/stacks.js +97 -0
- package/dist/stdlib.js +100 -0
- package/dist/terminal-images.js +46 -0
- package/dist/tools.js +242 -0
- package/dist/validate-cli.js +52 -0
- package/docs/README.md +58 -0
- package/docs/authoring-plain.md +245 -0
- package/docs/config.md +353 -0
- package/docs/deploy.md +109 -0
- package/docs/development.md +75 -0
- package/docs/headless.md +148 -0
- package/docs/images/how-turri-works.svg +50 -0
- package/docs/images/turri-banner.png +0 -0
- package/docs/installation.md +118 -0
- package/docs/parser.md +108 -0
- package/docs/regenerating.md +1116 -0
- package/docs/sandbox.md +233 -0
- package/docs/skills-and-gates.md +254 -0
- package/docs/theming.md +76 -0
- package/docs/using.md +454 -0
- package/genome/rules/bullet-continuation.md +69 -0
- package/genome/rules/concise-specs-examples.md +431 -0
- package/genome/rules/concise-specs-integration-examples.md +270 -0
- package/genome/rules/concise-specs.md +80 -0
- package/genome/rules/definitions.md +107 -0
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
- package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
- package/genome/rules/exported-concepts.md +39 -0
- package/genome/rules/func-specs.md +129 -0
- package/genome/rules/impl-reqs.md +68 -0
- package/genome/rules/import-modules.md +48 -0
- package/genome/rules/integration-embedded-testing.md +276 -0
- package/genome/rules/integration-embedded.md +171 -0
- package/genome/rules/integration-standalone.md +135 -0
- package/genome/rules/integrations.md +137 -0
- package/genome/rules/linked-resources.md +69 -0
- package/genome/rules/module-structure.md +105 -0
- package/genome/rules/project-model.md +88 -0
- package/genome/rules/required-concepts.md +44 -0
- package/genome/rules/requires-modules.md +80 -0
- package/genome/rules/stdlib.md +73 -0
- package/genome/rules/test-reqs.md +57 -0
- package/genome/skills/add-acceptance-test/SKILL.md +100 -0
- package/genome/skills/add-concept/SKILL.md +71 -0
- package/genome/skills/add-functional-spec/SKILL.md +84 -0
- package/genome/skills/add-functional-specs/SKILL.md +118 -0
- package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
- package/genome/skills/add-resource/SKILL.md +105 -0
- package/genome/skills/add-template/SKILL.md +64 -0
- package/genome/skills/add-test-requirement/SKILL.md +73 -0
- package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
- package/genome/skills/analyze-func-specs/SKILL.md +123 -0
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
- package/genome/skills/break-down-func-spec/SKILL.md +205 -0
- package/genome/skills/brood/SKILL.md +229 -0
- package/genome/skills/brood/references/intent.md +35 -0
- package/genome/skills/brood/references/product.md +101 -0
- package/genome/skills/brood/references/stack.md +61 -0
- package/genome/skills/brood/references/testing.md +56 -0
- package/genome/skills/bud/SKILL.md +148 -0
- package/genome/skills/bud/references/checklist.md +44 -0
- package/genome/skills/bud/references/intent.md +30 -0
- package/genome/skills/check-plain-env/SKILL.md +285 -0
- package/genome/skills/consolidate-concepts/SKILL.md +192 -0
- package/genome/skills/create-import-module/SKILL.md +104 -0
- package/genome/skills/create-requires-module/SKILL.md +109 -0
- package/genome/skills/debug-specs/SKILL.md +188 -0
- package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
- package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
- package/genome/skills/init-plain-project/SKILL.md +117 -0
- package/genome/skills/init-plain-project/references/checklist.md +30 -0
- package/genome/skills/mature/SKILL.md +126 -0
- package/genome/skills/refactor-module/SKILL.md +196 -0
- package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
- package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
- package/genome/skills/spawn/SKILL.md +163 -0
- package/package.json +68 -4
- package/stdlib/backend/common.plain +25 -0
- package/stdlib/backend/conformance/common.plain +12 -0
- package/stdlib/backend/conformance/go-gin.plain +9 -0
- package/stdlib/backend/conformance/go-nethttp.plain +9 -0
- package/stdlib/backend/conformance/java-springboot.plain +9 -0
- package/stdlib/backend/conformance/python-django.plain +9 -0
- package/stdlib/backend/conformance/python-fastapi.plain +9 -0
- package/stdlib/backend/conformance/python-flask.plain +9 -0
- package/stdlib/backend/conformance/rust-axum.plain +9 -0
- package/stdlib/backend/conformance/typescript-express.plain +9 -0
- package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
- package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
- package/stdlib/backend/go-gin.plain +18 -0
- package/stdlib/backend/go-nethttp.plain +18 -0
- package/stdlib/backend/java-springboot.plain +19 -0
- package/stdlib/backend/python-django.plain +19 -0
- package/stdlib/backend/python-fastapi.plain +23 -0
- package/stdlib/backend/python-flask.plain +23 -0
- package/stdlib/backend/rust-axum.plain +17 -0
- package/stdlib/backend/typescript-express.plain +19 -0
- package/stdlib/backend/typescript-fastify.plain +19 -0
- package/stdlib/backend/typescript-nestjs.plain +19 -0
- package/stdlib/cli/common.plain +17 -0
- package/stdlib/cli/conformance/common.plain +11 -0
- package/stdlib/cli/conformance/go-cobra.plain +9 -0
- package/stdlib/cli/conformance/go-flag.plain +9 -0
- package/stdlib/cli/conformance/java-picocli.plain +9 -0
- package/stdlib/cli/conformance/python-argparse.plain +9 -0
- package/stdlib/cli/conformance/python-click.plain +9 -0
- package/stdlib/cli/conformance/python-typer.plain +9 -0
- package/stdlib/cli/conformance/rust-clap.plain +9 -0
- package/stdlib/cli/conformance/typescript-commander.plain +9 -0
- package/stdlib/cli/go-cobra.plain +14 -0
- package/stdlib/cli/go-flag.plain +14 -0
- package/stdlib/cli/java-picocli.plain +15 -0
- package/stdlib/cli/python-argparse.plain +18 -0
- package/stdlib/cli/python-click.plain +16 -0
- package/stdlib/cli/python-typer.plain +16 -0
- package/stdlib/cli/rust-clap.plain +14 -0
- package/stdlib/cli/typescript-commander.plain +17 -0
- package/stdlib/frontend/angular.plain +17 -0
- package/stdlib/frontend/common.plain +17 -0
- package/stdlib/frontend/conformance/angular.plain +5 -0
- package/stdlib/frontend/conformance/common.plain +11 -0
- package/stdlib/frontend/conformance/react-vite.plain +5 -0
- package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
- package/stdlib/frontend/conformance/vue-vite.plain +5 -0
- package/stdlib/frontend/react-vite.plain +16 -0
- package/stdlib/frontend/sveltekit-spa.plain +16 -0
- package/stdlib/frontend/vue-vite.plain +16 -0
- package/stdlib/lang/go.plain +13 -0
- package/stdlib/lang/java.plain +12 -0
- package/stdlib/lang/python.plain +18 -0
- package/stdlib/lang/rust.plain +12 -0
- package/stdlib/lang/typescript.plain +13 -0
- package/stdlib/library/common.plain +15 -0
- package/stdlib/library/conformance/common.plain +9 -0
- package/stdlib/library/conformance/go.plain +9 -0
- package/stdlib/library/conformance/java-gradle.plain +9 -0
- package/stdlib/library/conformance/java-maven.plain +9 -0
- package/stdlib/library/conformance/python.plain +9 -0
- package/stdlib/library/conformance/rust.plain +9 -0
- package/stdlib/library/conformance/typescript.plain +9 -0
- package/stdlib/library/go.plain +12 -0
- package/stdlib/library/java-gradle.plain +13 -0
- package/stdlib/library/java-maven.plain +13 -0
- package/stdlib/library/python.plain +13 -0
- package/stdlib/library/rust.plain +12 -0
- package/stdlib/library/typescript.plain +15 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { availableParallelism } from "node:os";
|
|
3
|
+
import { withPulse } from "./shell.js";
|
|
4
|
+
import { describeEnvironmentError, EnvironmentError, missingCredentials } from "./environment-error.js";
|
|
5
|
+
import { addAgentReport, describeAgents, JobStoppedError, withActivity } from "./activity.js";
|
|
6
|
+
import { startJobLog } from "./job-log.js";
|
|
7
|
+
import { chosenStack, withStack } from "../config.js";
|
|
8
|
+
const DEFAULT_TAIL_CHARS = 4000;
|
|
9
|
+
const ACTIVITY_LINES = 10;
|
|
10
|
+
/** A long render's full transcript is kept, up to about this much text. */
|
|
11
|
+
const MAX_TRANSCRIPT_CHARS = 600_000;
|
|
12
|
+
/** What a stopped job says when the person stopped it. */
|
|
13
|
+
export const STOPPED_ON_REQUEST = "Stopped on request.";
|
|
14
|
+
export function createJobRegistry(options) {
|
|
15
|
+
const jobs = new Map();
|
|
16
|
+
// The slots: how many jobs run, and the ones whose lanes are free waiting for one, in the order they got there.
|
|
17
|
+
let limit = options?.maxRunning !== undefined && options.maxRunning >= 1 ? Math.floor(options.maxRunning) : undefined;
|
|
18
|
+
let active = 0;
|
|
19
|
+
const waiters = [];
|
|
20
|
+
const takeSlot = () => new Promise((resolve) => {
|
|
21
|
+
if (limit === undefined || active < limit) {
|
|
22
|
+
active += 1;
|
|
23
|
+
resolve();
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
waiters.push(() => ((active += 1), resolve()));
|
|
27
|
+
});
|
|
28
|
+
const giveSlot = () => {
|
|
29
|
+
active -= 1;
|
|
30
|
+
waiters.shift()?.();
|
|
31
|
+
};
|
|
32
|
+
const settled = new Map();
|
|
33
|
+
const tailChars = options?.tailChars ?? DEFAULT_TAIL_CHARS;
|
|
34
|
+
/** The last job to claim each lane. A new job waits for it and becomes the new tail. */
|
|
35
|
+
const lanes = new Map();
|
|
36
|
+
const controllers = new Map();
|
|
37
|
+
/** Why turri stopped a job, when it did: a job stopped without a reason was stopped by the person. */
|
|
38
|
+
const stopReasons = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Jobs whose work is over. Not the same as a finished status: a queued job that was
|
|
41
|
+
* stopped says "stopped" at once, but still sits in its lanes until the job ahead of it
|
|
42
|
+
* settles - and whatever queues behind it has to wait that long too.
|
|
43
|
+
*/
|
|
44
|
+
const done = new Set();
|
|
45
|
+
let sequence = 0;
|
|
46
|
+
return {
|
|
47
|
+
setMaxRunning(count) {
|
|
48
|
+
limit = count !== undefined && count >= 1 ? Math.floor(count) : undefined;
|
|
49
|
+
// Raised: the jobs waiting for a slot that now has room start.
|
|
50
|
+
while (waiters.length > 0 && (limit === undefined || active < limit))
|
|
51
|
+
waiters.shift()();
|
|
52
|
+
},
|
|
53
|
+
start(input) {
|
|
54
|
+
const id = randomUUID().slice(0, 8);
|
|
55
|
+
const claimed = [...new Set(input.lanes ?? [])];
|
|
56
|
+
const blockers = [
|
|
57
|
+
...new Set(claimed
|
|
58
|
+
.map((lane) => lanes.get(lane))
|
|
59
|
+
.filter((other) => other !== undefined && jobs.has(other) && !done.has(other))),
|
|
60
|
+
];
|
|
61
|
+
for (const lane of claimed)
|
|
62
|
+
lanes.set(lane, id);
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const job = {
|
|
65
|
+
id,
|
|
66
|
+
kind: input.kind,
|
|
67
|
+
summary: input.summary,
|
|
68
|
+
...(input.module !== undefined ? { module: input.module } : {}),
|
|
69
|
+
...(input.pulse !== undefined ? { pulse: input.pulse } : {}),
|
|
70
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
71
|
+
diff: input.diff,
|
|
72
|
+
model: input.model,
|
|
73
|
+
...(input.follows !== undefined ? { follows: input.follows } : {}),
|
|
74
|
+
status: blockers.length > 0 || (limit !== undefined && active >= limit) ? "queued" : "running",
|
|
75
|
+
sequence: (sequence += 1),
|
|
76
|
+
queuedAt: now,
|
|
77
|
+
startedAt: now,
|
|
78
|
+
waitingOn: blockers,
|
|
79
|
+
tail: "",
|
|
80
|
+
activity: [],
|
|
81
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
|
|
82
|
+
agents: [],
|
|
83
|
+
transcript: [],
|
|
84
|
+
};
|
|
85
|
+
jobs.set(job.id, job);
|
|
86
|
+
let transcriptChars = 0;
|
|
87
|
+
const changed = () => {
|
|
88
|
+
// Like onFinish: a display that throws must not take the job down.
|
|
89
|
+
try {
|
|
90
|
+
options?.onChange?.(job);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
changed();
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
controllers.set(id, controller);
|
|
99
|
+
/** The job's transcript on disk, from when it starts running - see job-log.ts. */
|
|
100
|
+
let log;
|
|
101
|
+
const sink = {
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
job: { id, kind: input.kind, summary: input.summary },
|
|
104
|
+
specConflict(conflict) {
|
|
105
|
+
job.specConflict = conflict;
|
|
106
|
+
this.transcript({ kind: "stage", text: `the spec contradicts itself: functionalities ${conflict.specs.map((spec) => `${spec.module} ${spec.frid}`).join(" and ")} cannot both hold` });
|
|
107
|
+
changed();
|
|
108
|
+
},
|
|
109
|
+
stage(label) {
|
|
110
|
+
job.stage = label;
|
|
111
|
+
this.transcript({ kind: "stage", text: label });
|
|
112
|
+
changed();
|
|
113
|
+
},
|
|
114
|
+
spec(spec) {
|
|
115
|
+
// The same functionality going on to its next stage keeps its clock; another starts a new one.
|
|
116
|
+
const since = spec !== undefined && job.building?.label === spec.label ? job.building.since : Date.now();
|
|
117
|
+
job.building = spec === undefined ? undefined : { ...spec, since };
|
|
118
|
+
if (spec !== undefined)
|
|
119
|
+
this.transcript({ kind: "spec", text: `${spec.label}\n${spec.text}` });
|
|
120
|
+
changed();
|
|
121
|
+
},
|
|
122
|
+
transcript(entry) {
|
|
123
|
+
log?.entry(entry);
|
|
124
|
+
// The screen keeps the clipped text; the log has the whole.
|
|
125
|
+
const { full: _full, ...shown } = entry;
|
|
126
|
+
entry = shown;
|
|
127
|
+
const last = job.transcript.at(-1);
|
|
128
|
+
// Deltas of a running reply join the entry they continue - the same agent's.
|
|
129
|
+
if ((entry.kind === "text" || entry.kind === "thinking") && last?.kind === entry.kind && last.agent?.label === entry.agent?.label) {
|
|
130
|
+
last.text += entry.text;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
job.transcript.push({ ...entry });
|
|
134
|
+
}
|
|
135
|
+
transcriptChars += entry.text.length;
|
|
136
|
+
while (transcriptChars > MAX_TRANSCRIPT_CHARS && job.transcript.length > 1) {
|
|
137
|
+
transcriptChars -= job.transcript.shift().text.length;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
activity(line) {
|
|
141
|
+
job.activity = [...job.activity, line].slice(-ACTIVITY_LINES);
|
|
142
|
+
changed();
|
|
143
|
+
},
|
|
144
|
+
note(line) {
|
|
145
|
+
log?.note(line);
|
|
146
|
+
},
|
|
147
|
+
usage(spent) {
|
|
148
|
+
job.usage = {
|
|
149
|
+
input: job.usage.input + spent.input,
|
|
150
|
+
output: job.usage.output + spent.output,
|
|
151
|
+
cacheRead: job.usage.cacheRead + spent.cacheRead,
|
|
152
|
+
cacheWrite: job.usage.cacheWrite + spent.cacheWrite,
|
|
153
|
+
cost: job.usage.cost + spent.cost,
|
|
154
|
+
};
|
|
155
|
+
changed();
|
|
156
|
+
},
|
|
157
|
+
agent(report) {
|
|
158
|
+
addAgentReport(job.agents, report);
|
|
159
|
+
},
|
|
160
|
+
now(label, opts) {
|
|
161
|
+
job.now =
|
|
162
|
+
label === undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: { label, since: opts?.keepSince && job.now ? job.now.since : Date.now() };
|
|
165
|
+
changed();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
// Whether the job ahead succeeded or failed, this one gets its turn: each reports on its own -
|
|
169
|
+
// unless the registry retries, and one ahead of it failed for good: then nothing behind it runs.
|
|
170
|
+
const turn = Promise.allSettled(blockers.map((other) => settled.get(other)));
|
|
171
|
+
const guarded = options?.retries !== undefined;
|
|
172
|
+
const retries = options?.retries ?? 0;
|
|
173
|
+
/** A job's work ran, and did not do what it was asked: it threw, or said it did not pass. */
|
|
174
|
+
const didNotPass = (outcome) => {
|
|
175
|
+
const details = outcome.details;
|
|
176
|
+
return details?.passed === false && details.skipped !== true;
|
|
177
|
+
};
|
|
178
|
+
// A pulse: what its agents leave running is killed when it ends, and the modules it works in are its own.
|
|
179
|
+
const runOnce = () => {
|
|
180
|
+
// A credential agent-env names that is not there stops the pulse before any agent starts.
|
|
181
|
+
const own = input.lanes?.[0];
|
|
182
|
+
const missing = own !== undefined ? missingCredentials(own) : undefined;
|
|
183
|
+
if (missing !== undefined)
|
|
184
|
+
return Promise.reject(missing);
|
|
185
|
+
return withPulse(() => withActivity(sink, () => input.run((delta) => {
|
|
186
|
+
job.tail = (job.tail + delta).slice(-tailChars);
|
|
187
|
+
})), { modules: input.lanes ?? [] });
|
|
188
|
+
};
|
|
189
|
+
const runGuarded = async () => {
|
|
190
|
+
const failures = [];
|
|
191
|
+
for (let attempt = 0;; attempt += 1) {
|
|
192
|
+
let outcome;
|
|
193
|
+
let error;
|
|
194
|
+
try {
|
|
195
|
+
outcome = await runOnce();
|
|
196
|
+
}
|
|
197
|
+
catch (thrown) {
|
|
198
|
+
error = thrown;
|
|
199
|
+
}
|
|
200
|
+
// Asked to stop is not a failure: no retry, and nothing behind it fails for it.
|
|
201
|
+
if (controller.signal.aborted) {
|
|
202
|
+
if (error !== undefined)
|
|
203
|
+
throw error;
|
|
204
|
+
return outcome;
|
|
205
|
+
}
|
|
206
|
+
if (error === undefined && !didNotPass(outcome))
|
|
207
|
+
return outcome;
|
|
208
|
+
// The environment lacks something: no retry would find it - the pulse stops, the module's later pulses wait,
|
|
209
|
+
// and the person is told. What the harness found itself is worked out at once; what an agent said goes as it is.
|
|
210
|
+
if (error instanceof EnvironmentError) {
|
|
211
|
+
job.failedForGood = true;
|
|
212
|
+
if (error.source === "harness" && options?.diagnose !== undefined) {
|
|
213
|
+
sink.stage("the environment lacks something - working out what to do about it");
|
|
214
|
+
job.diagnosis = await withActivity(sink, () => options.diagnose(job, [error.message, describeEnvironmentError(error, input.rerun)], log?.file)).catch(() => undefined);
|
|
215
|
+
}
|
|
216
|
+
job.environment = describeEnvironmentError(error, input.rerun);
|
|
217
|
+
throw new Error([job.environment, ...(job.diagnosis !== undefined ? ["", job.diagnosis] : [])].join("\n"));
|
|
218
|
+
}
|
|
219
|
+
failures.push(error !== undefined ? (error instanceof Error ? error.message : String(error)) : outcome.report);
|
|
220
|
+
if (attempt < retries) {
|
|
221
|
+
sink.stage(`the pulse failed - trying it once more (${attempt + 1} of ${retries})`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
job.failedForGood = true;
|
|
225
|
+
if (options?.diagnose !== undefined) {
|
|
226
|
+
sink.stage("the pulse failed again - working out why, and what has to be done");
|
|
227
|
+
job.diagnosis = await withActivity(sink, () => options.diagnose(job, failures, log?.file)).catch(() => undefined);
|
|
228
|
+
}
|
|
229
|
+
throw new Error([
|
|
230
|
+
`Failed ${failures.length} time${failures.length === 1 ? "" : "s"}, so it and every pulse queued behind it have stopped.`,
|
|
231
|
+
...(job.diagnosis !== undefined ? ["", job.diagnosis] : []),
|
|
232
|
+
"",
|
|
233
|
+
"The last attempt ended with:",
|
|
234
|
+
failures.at(-1).trim().slice(-4000),
|
|
235
|
+
].join("\n"));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
/** It holds one of the run slots, to give back when it ends. */
|
|
239
|
+
let holding = false;
|
|
240
|
+
const promise = turn
|
|
241
|
+
.then(async () => {
|
|
242
|
+
// Stopped while it waited: it never starts.
|
|
243
|
+
if (controller.signal.aborted)
|
|
244
|
+
throw new JobStoppedError();
|
|
245
|
+
// A job ahead of it failed for good: it does not start, and fails for good too - so does what waits on it.
|
|
246
|
+
const failedAhead = guarded ? blockers.map((other) => jobs.get(other)).find((other) => other?.failedForGood === true) : undefined;
|
|
247
|
+
if (failedAhead !== undefined) {
|
|
248
|
+
job.failedForGood = true;
|
|
249
|
+
if (failedAhead.environment !== undefined) {
|
|
250
|
+
throw new Error(`Not run: pulse ${failedAhead.id} ahead of it in the queue (${pulseName(failedAhead)}) stopped, and this waits for it.\n\n${failedAhead.environment}`);
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`Not run: pulse ${failedAhead.id} ahead of it in the queue (${pulseName(failedAhead)}) failed twice, so nothing queued behind it runs.` +
|
|
253
|
+
(failedAhead.diagnosis !== undefined ? `\n\n${failedAhead.diagnosis}` : ""));
|
|
254
|
+
}
|
|
255
|
+
// Its lanes are free; it still needs one of the run slots.
|
|
256
|
+
if (limit !== undefined && active >= limit) {
|
|
257
|
+
job.status = "queued";
|
|
258
|
+
job.waitingOn = [];
|
|
259
|
+
job.stage = `waiting for a free slot - ${active} of ${limit} pulses running at once (parallel-jobs)`;
|
|
260
|
+
changed();
|
|
261
|
+
}
|
|
262
|
+
await takeSlot();
|
|
263
|
+
holding = true;
|
|
264
|
+
if (controller.signal.aborted)
|
|
265
|
+
throw new JobStoppedError();
|
|
266
|
+
if (job.status === "queued") {
|
|
267
|
+
job.status = "running";
|
|
268
|
+
job.startedAt = Date.now();
|
|
269
|
+
job.waitingOn = [];
|
|
270
|
+
job.stage = undefined;
|
|
271
|
+
changed();
|
|
272
|
+
}
|
|
273
|
+
log = startJobLog(job, claimed);
|
|
274
|
+
if (log !== undefined)
|
|
275
|
+
job.logFile = log.file;
|
|
276
|
+
// What the job's agents start runs until it ends, and no longer (withPulse) - with the stack chosen as it
|
|
277
|
+
// starts, whatever is chosen while it runs.
|
|
278
|
+
return withStack(chosenStack(), () => (guarded ? runGuarded() : runOnce()));
|
|
279
|
+
})
|
|
280
|
+
.then((outcome) => {
|
|
281
|
+
// Asked to stop, it is stopped - whether its work threw as it unwound or returned what it had.
|
|
282
|
+
job.status = controller.signal.aborted ? "stopped" : "succeeded";
|
|
283
|
+
if (controller.signal.aborted)
|
|
284
|
+
job.error = stopReasons.get(id) ?? STOPPED_ON_REQUEST;
|
|
285
|
+
job.report = outcome.report;
|
|
286
|
+
job.announce = outcome.announce;
|
|
287
|
+
job.details = outcome.details;
|
|
288
|
+
return job;
|
|
289
|
+
}, (error) => {
|
|
290
|
+
// Asked to stop is not a failure, whatever the work threw as it unwound.
|
|
291
|
+
job.status = controller.signal.aborted ? "stopped" : "failed";
|
|
292
|
+
job.error = controller.signal.aborted ? (stopReasons.get(id) ?? STOPPED_ON_REQUEST) : error instanceof Error ? error.message : String(error);
|
|
293
|
+
return job;
|
|
294
|
+
})
|
|
295
|
+
.then((finished) => {
|
|
296
|
+
if (holding) {
|
|
297
|
+
holding = false;
|
|
298
|
+
giveSlot();
|
|
299
|
+
}
|
|
300
|
+
finished.finishedAt = Date.now();
|
|
301
|
+
log?.end(finished.status === "succeeded" ? "finished" : finished.status === "stopped" ? "stopped" : "finished - failed", finished.announce ?? finished.report ?? finished.error, describeAgents(finished.agents));
|
|
302
|
+
// A failing listener must not take the job down with it; the result is
|
|
303
|
+
// already recorded and can still be collected by asking.
|
|
304
|
+
try {
|
|
305
|
+
options?.onFinish?.(finished);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
/* ignore */
|
|
309
|
+
}
|
|
310
|
+
return finished;
|
|
311
|
+
});
|
|
312
|
+
settled.set(job.id, promise);
|
|
313
|
+
promise.then(() => {
|
|
314
|
+
controllers.delete(id);
|
|
315
|
+
done.add(id);
|
|
316
|
+
// Release the lanes this job still holds, so a later job does not wait on a ghost.
|
|
317
|
+
for (const lane of claimed)
|
|
318
|
+
if (lanes.get(lane) === id)
|
|
319
|
+
lanes.delete(lane);
|
|
320
|
+
});
|
|
321
|
+
return job;
|
|
322
|
+
},
|
|
323
|
+
get(id) {
|
|
324
|
+
return jobs.get(id);
|
|
325
|
+
},
|
|
326
|
+
stop(id, reason) {
|
|
327
|
+
const job = jobs.get(id);
|
|
328
|
+
const controller = controllers.get(id);
|
|
329
|
+
if (job === undefined || controller === undefined || done.has(id) || job.status === "stopped")
|
|
330
|
+
return undefined;
|
|
331
|
+
// Who stopped it, and why: turri, saying so - or, without a reason, the person.
|
|
332
|
+
if (reason !== undefined)
|
|
333
|
+
stopReasons.set(id, reason);
|
|
334
|
+
controller.abort();
|
|
335
|
+
// A queued job is done with at once, so the queue behind it shows it gone; a running
|
|
336
|
+
// one stays running until its agent has actually stopped.
|
|
337
|
+
if (job.status === "queued") {
|
|
338
|
+
job.status = "stopped";
|
|
339
|
+
job.waitingOn = [];
|
|
340
|
+
job.error = stopReasons.get(id) ?? STOPPED_ON_REQUEST;
|
|
341
|
+
options?.onChange?.(job);
|
|
342
|
+
}
|
|
343
|
+
return job;
|
|
344
|
+
},
|
|
345
|
+
list() {
|
|
346
|
+
return [...jobs.values()].sort((a, b) => b.sequence - a.sequence);
|
|
347
|
+
},
|
|
348
|
+
notify() {
|
|
349
|
+
try {
|
|
350
|
+
options?.onNotify?.();
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* a display that throws must not take the work down */
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
async wait(id) {
|
|
357
|
+
const promise = settled.get(id);
|
|
358
|
+
if (promise === undefined)
|
|
359
|
+
throw new Error(`No such pulse: ${id}`);
|
|
360
|
+
return promise;
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
export function formatDuration(job) {
|
|
365
|
+
return formatElapsed((job.finishedAt ?? Date.now()) - job.startedAt);
|
|
366
|
+
}
|
|
367
|
+
/** "42s", "3m 12s", "1h 5m": how long something has run. */
|
|
368
|
+
export function formatElapsed(ms) {
|
|
369
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
370
|
+
if (seconds < 60)
|
|
371
|
+
return `${seconds}s`;
|
|
372
|
+
if (seconds < 3600)
|
|
373
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
374
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* What a pulse is called wherever it is shown in a line: its module, kind and number - `shorty · regenerate pulse 3`,
|
|
378
|
+
* `shorty · conformance pulse 6` - never what it regenerates, which is the agents' words and long. A job of no module,
|
|
379
|
+
* what was asked for.
|
|
380
|
+
*/
|
|
381
|
+
export function pulseName(job) {
|
|
382
|
+
if (job.module === undefined)
|
|
383
|
+
return job.summary;
|
|
384
|
+
return job.pulse !== undefined ? `${job.module} · ${job.kind} pulse ${job.pulse}` : `${job.module} ${job.kind}`;
|
|
385
|
+
}
|
|
386
|
+
/** What the agent is told when it checks on a job. */
|
|
387
|
+
export function describeJob(job) {
|
|
388
|
+
if (job.status === "queued") {
|
|
389
|
+
return `${pulseName(job)}: queued behind ${job.waitingOn.map((id) => `pulse ${id}`).join(" and ")}. It starts when that finishes.`;
|
|
390
|
+
}
|
|
391
|
+
const header = `${pulseName(job)}: ${job.status}, ${formatDuration(job)}`;
|
|
392
|
+
if (job.status === "running") {
|
|
393
|
+
return [
|
|
394
|
+
header,
|
|
395
|
+
...(job.building ? ["", `building ${job.building.label}:`, job.building.text] : []),
|
|
396
|
+
...(job.stage ? ["", `stage: ${job.stage}`] : []),
|
|
397
|
+
...(job.activity.length > 0 ? ["", "<recent activity>", ...job.activity, "</recent activity>"] : []),
|
|
398
|
+
"",
|
|
399
|
+
"<latest output>",
|
|
400
|
+
job.tail.trimEnd() || "(nothing yet)",
|
|
401
|
+
"</latest output>",
|
|
402
|
+
].join("\n");
|
|
403
|
+
}
|
|
404
|
+
if (job.status === "failed")
|
|
405
|
+
return `${header}\n\n${job.error ?? "no error recorded"}`;
|
|
406
|
+
if (job.status === "stopped")
|
|
407
|
+
return `${header}\n\n${job.error ?? STOPPED_ON_REQUEST}`;
|
|
408
|
+
const agents = describeAgents(job.agents ?? []);
|
|
409
|
+
return `${header}\n\n${job.report ?? "(no report)"}${agents.length > 0 ? `\n\n<its agents>\n${agents.join("\n")}\n</its agents>` : ""}`;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Whether a tool should block rather than hand back a job id.
|
|
413
|
+
*
|
|
414
|
+
* Background only makes sense where the session outlives the turn. In print, json and rpc
|
|
415
|
+
* modes the process ends as soon as the answer is written, taking any detached work with
|
|
416
|
+
* it, so those wait unless told otherwise.
|
|
417
|
+
*/
|
|
418
|
+
export function shouldWait(mode, wait) {
|
|
419
|
+
return wait ?? mode !== "tui";
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* How many jobs run at once when nothing says: half this machine's cores, at least one and at most four - a pulse's
|
|
423
|
+
* builds and tests run here, beside the person's own work.
|
|
424
|
+
*/
|
|
425
|
+
export function defaultParallelJobs(cores = availableParallelism()) {
|
|
426
|
+
return Math.min(4, Math.max(1, Math.floor(cores / 2)));
|
|
427
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { basename, dirname, resolve } from "node:path";
|
|
2
|
+
import { buildDirUnder, modulesRoot, projectDirOf } from "./paths.js";
|
|
3
|
+
import { resolveRequiresChain } from "./requires.js";
|
|
4
|
+
/**
|
|
5
|
+
* What a job on this spec may write, as lanes for the job registry.
|
|
6
|
+
*
|
|
7
|
+
* A module's directory holds its code and its conformance tests, and building a module can
|
|
8
|
+
* build everything it requires first - so the lanes are every module in the requires chain.
|
|
9
|
+
* That is more than a single functionality touches, and deliberately so: two jobs that
|
|
10
|
+
* might write the same directory wait for each other, and a wait costs minutes where a
|
|
11
|
+
* collision costs the build.
|
|
12
|
+
*
|
|
13
|
+
* The module's own folder is a lane whatever the spec says at the moment it is read. A spec with
|
|
14
|
+
* no functional specs yet - the scaffold a new project starts with - or one that does not parse
|
|
15
|
+
* names no chain, and a job queued then must still wait for, and be waited for by, every other
|
|
16
|
+
* job on that module: two that claimed different lanes for the same folder ran side by side and
|
|
17
|
+
* wrote over each other.
|
|
18
|
+
*/
|
|
19
|
+
export function moduleLanes(plainFile, buildRoot) {
|
|
20
|
+
const extra = buildRoot !== undefined ? [resolve(buildRoot)] : [];
|
|
21
|
+
// The folder the module builds in, by its name - the file's - as every chain names it too.
|
|
22
|
+
const own = dirname(buildDirUnder(resolve(buildRoot ?? modulesRoot(projectDirOf(plainFile))), basename(plainFile, ".plain")));
|
|
23
|
+
try {
|
|
24
|
+
const chain = resolveRequiresChain(plainFile, [], buildRoot, { allowNoFunctionalities: true });
|
|
25
|
+
return [...new Set([own, ...chain.map((entry) => dirname(entry.buildDir)), ...extra])];
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A spec that does not parse fails inside the job with a proper error. Until then it
|
|
29
|
+
// still must not run alongside another job on the same module, or on the same file.
|
|
30
|
+
return [...new Set([own, resolve(plainFile), ...extra])];
|
|
31
|
+
}
|
|
32
|
+
}
|