@turri-dev/cli 0.1.7 → 0.1.8-next.b03162b
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 +5 -7
- package/dist/btw.d.ts +2 -0
- package/dist/btw.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/config-edit.d.ts +1 -1
- package/dist/config-edit.js +2 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +5 -1
- package/dist/extension.d.ts +11 -0
- package/dist/extension.js +112 -13
- package/dist/host.d.ts +2 -0
- package/dist/host.js +3 -0
- package/dist/identity.d.ts +6 -1
- package/dist/identity.js +33 -7
- package/dist/init-config.d.ts +1 -0
- package/dist/init-config.js +5 -0
- package/dist/ledger-cli.d.ts +1 -0
- package/dist/ledger-cli.js +50 -0
- package/dist/loop-cli.js +4 -0
- package/dist/loop-tools.js +3 -1
- package/dist/pi.js +17 -0
- package/dist/regenerate/activity.d.ts +9 -0
- package/dist/regenerate/activity.js +11 -0
- package/dist/regenerate/agent-shell.js +5 -0
- package/dist/regenerate/cases.js +3 -0
- package/dist/regenerate/chunk-plan.d.ts +0 -6
- package/dist/regenerate/chunk-plan.js +16 -4
- package/dist/regenerate/conformance-agents.js +2 -2
- package/dist/regenerate/conformance.js +6 -3
- package/dist/regenerate/diagnose.js +4 -2
- package/dist/regenerate/environment-error.d.ts +6 -0
- package/dist/regenerate/environment-error.js +19 -1
- package/dist/regenerate/job-log.d.ts +3 -1
- package/dist/regenerate/job-log.js +8 -5
- package/dist/regenerate/jobs-tree.d.ts +7 -3
- package/dist/regenerate/jobs-tree.js +21 -6
- package/dist/regenerate/jobs.d.ts +18 -0
- package/dist/regenerate/jobs.js +49 -3
- package/dist/regenerate/ledger-view.d.ts +73 -0
- package/dist/regenerate/ledger-view.js +286 -0
- package/dist/regenerate/ledger.d.ts +274 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.d.ts +9 -1
- package/dist/regenerate/lineage.js +64 -12
- package/dist/regenerate/loop.d.ts +6 -0
- package/dist/regenerate/loop.js +36 -6
- package/dist/regenerate/records.d.ts +5 -3
- package/dist/regenerate/records.js +6 -4
- package/dist/regenerate/regenerate.js +7 -5
- package/dist/regenerate/regression.js +4 -2
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/spec-map.js +8 -5
- package/dist/regenerate/test-scripts.js +12 -1
- package/dist/regenerate/unit-review.js +3 -0
- package/dist/run-report.d.ts +1 -1
- package/dist/run-report.js +6 -0
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +3 -2
- package/dist/skill-progress.d.ts +1 -1
- package/dist/skill.d.ts +5 -0
- package/dist/skill.js +1 -0
- package/dist/skills.d.ts +14 -0
- package/dist/skills.js +19 -1
- package/dist/surface.d.ts +5 -0
- package/dist/surface.js +18 -0
- package/docs/authoring-plain.md +2 -2
- package/docs/config.md +4 -2
- package/docs/development.md +1 -0
- package/docs/headless.md +4 -0
- package/docs/hosting.md +45 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +5 -5
- package/docs/skills-and-gates.md +3 -1
- package/docs/using.md +33 -5
- package/genome/rules/func-specs.md +1 -0
- package/genome/rules/turri-regeneration.md +3 -0
- package/genome/skills/add-functional-spec/SKILL.md +12 -3
- package/genome/skills/add-functional-specs/SKILL.md +12 -2
- package/genome/skills/brood/SKILL.md +2 -0
- package/genome/skills/bud/SKILL.md +2 -0
- package/genome/skills/spawn/SKILL.md +47 -35
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { reportActivity } from "./activity.js";
|
|
5
|
+
import { historyError } from "./environment-error.js";
|
|
6
|
+
import { firstLine, record } from "./ledger.js";
|
|
5
7
|
import { projectRootOf } from "./paths.js";
|
|
6
8
|
/**
|
|
7
9
|
* Version history for each module's generated code and tests.
|
|
@@ -15,7 +17,8 @@ import { projectRootOf } from "./paths.js";
|
|
|
15
17
|
* A commit is made after every step that changes the module - a render, a regeneration, a
|
|
16
18
|
* conformance run, a regression repair, a spec change, a start from the module below - with
|
|
17
19
|
* a message saying what it was. History is a record of the work, never a gate on it: a git
|
|
18
|
-
* failure is swallowed, not raised
|
|
20
|
+
* failure is swallowed, not raised - except landing work (`landWork`), whose commit the next
|
|
21
|
+
* start would otherwise undo.
|
|
19
22
|
*/
|
|
20
23
|
export const HISTORY_DIR = ".plaingit";
|
|
21
24
|
const AUTHOR = ["-c", "user.name=turri", "-c", "user.email=turri@localhost", "-c", "commit.gpgsign=false"];
|
|
@@ -39,7 +42,10 @@ const EXCLUDED = [
|
|
|
39
42
|
".DS_Store",
|
|
40
43
|
];
|
|
41
44
|
function git(moduleDir, args, input) {
|
|
42
|
-
|
|
45
|
+
const gitDir = join(moduleDir, HISTORY_DIR);
|
|
46
|
+
// The history is turri's, not the person's: hooks their git config names - gitleaks, husky - would refuse some of its
|
|
47
|
+
// commits, and the next start would put back what landed. Its own hooks folder holds only git's samples.
|
|
48
|
+
return spawnSync("git", [...AUTHOR, "-c", `core.hooksPath=${join(gitDir, "hooks")}`, "--git-dir", gitDir, "--work-tree", moduleDir, ...args], {
|
|
43
49
|
cwd: moduleDir,
|
|
44
50
|
encoding: "utf8",
|
|
45
51
|
input,
|
|
@@ -79,21 +85,65 @@ function ensureHistory(moduleDir) {
|
|
|
79
85
|
*/
|
|
80
86
|
export function recordHistory(dir, subject, body) {
|
|
81
87
|
try {
|
|
82
|
-
|
|
83
|
-
if (!existsSync(moduleDir) || !ensureHistory(moduleDir))
|
|
84
|
-
return undefined;
|
|
85
|
-
git(moduleDir, ["add", "-A"]);
|
|
86
|
-
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
87
|
-
return undefined;
|
|
88
|
-
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
89
|
-
if (git(moduleDir, ["commit", "-q", "-F", "-"], message).status !== 0)
|
|
90
|
-
return undefined;
|
|
91
|
-
return head(dir);
|
|
88
|
+
return recorded(dir, subject, commitAll(dir, subject, body), false).commit;
|
|
92
89
|
}
|
|
93
90
|
catch {
|
|
94
91
|
return undefined;
|
|
95
92
|
}
|
|
96
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Commit work that landed - the code or tests a pulse passed, with the record of what they are built from. Unlike any
|
|
96
|
+
* other step's, this commit is not only a record: the next unit of work puts the module back to its last commit, and
|
|
97
|
+
* would undo what landed with it. So a commit that cannot be made is raised, as the environment's, with what git said -
|
|
98
|
+
* the work left in place, uncommitted. Nothing changed, nothing is committed. Returns the commit, if any.
|
|
99
|
+
*/
|
|
100
|
+
export function landWork(dir, subject, body) {
|
|
101
|
+
const moduleDir = moduleDirOf(dir);
|
|
102
|
+
let made;
|
|
103
|
+
try {
|
|
104
|
+
made = commitAll(dir, subject, body);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
made = { failed: error instanceof Error ? error.message : String(error) };
|
|
108
|
+
}
|
|
109
|
+
recorded(dir, subject, made, true);
|
|
110
|
+
if (made.failed !== undefined)
|
|
111
|
+
throw historyError(moduleDir, made.failed);
|
|
112
|
+
return made.commit;
|
|
113
|
+
}
|
|
114
|
+
/** A commit made or failed, in the ledger - nothing for a step that changed nothing. */
|
|
115
|
+
function recorded(dir, subject, made, landing) {
|
|
116
|
+
const place = placeOf(moduleDirOf(dir));
|
|
117
|
+
if (made.failed !== undefined)
|
|
118
|
+
record({ kind: "history.failed", subject: firstLine(subject, 200), said: firstLine(made.failed, 400), landing }, place);
|
|
119
|
+
else if (made.commit !== undefined)
|
|
120
|
+
record({ kind: "history.committed", subject: firstLine(subject, 200), commit: made.commit }, place);
|
|
121
|
+
return made;
|
|
122
|
+
}
|
|
123
|
+
/** Where a module's history is, for the ledger: its project and its name. */
|
|
124
|
+
function placeOf(moduleDir) {
|
|
125
|
+
return { project: projectRootOf(moduleDir), module: basename(moduleDir) };
|
|
126
|
+
}
|
|
127
|
+
/** Commit everything the step changed in the module `dir` belongs to: the commit, nothing when nothing changed, or why git failed. */
|
|
128
|
+
function commitAll(dir, subject, body) {
|
|
129
|
+
const moduleDir = moduleDirOf(dir);
|
|
130
|
+
if (!existsSync(moduleDir))
|
|
131
|
+
return {};
|
|
132
|
+
if (!ensureHistory(moduleDir))
|
|
133
|
+
return { failed: `git could not make the module's history in ${join(moduleDir, HISTORY_DIR)}` };
|
|
134
|
+
const failed = (result) => (result.error?.message ?? `${result.stderr}${result.stdout}`.trim()) || `git exited ${result.status}`;
|
|
135
|
+
const added = git(moduleDir, ["add", "-A"]);
|
|
136
|
+
if (added.status !== 0)
|
|
137
|
+
return { failed: failed(added) };
|
|
138
|
+
if (git(moduleDir, ["diff", "--cached", "--quiet"]).status === 0)
|
|
139
|
+
return {};
|
|
140
|
+
const message = body ? `${subject}\n\n${body.trim()}\n` : subject;
|
|
141
|
+
const committed = git(moduleDir, ["commit", "-q", "-F", "-"], message);
|
|
142
|
+
if (committed.status !== 0)
|
|
143
|
+
return { failed: failed(committed) };
|
|
144
|
+
const commit = head(dir);
|
|
145
|
+
return commit !== undefined ? { commit } : {};
|
|
146
|
+
}
|
|
97
147
|
/**
|
|
98
148
|
* The notes the harness writes once a pulse has ended, committed - or the next pulse, putting the module back to its last
|
|
99
149
|
* commit, would take them away. Into the pulse's own commit when it landed one - `landedSince` (ms), when it started - so
|
|
@@ -156,6 +206,7 @@ export function resetToLastCommit(dir) {
|
|
|
156
206
|
return [];
|
|
157
207
|
git(moduleDir, ["reset", "-q", "--hard", "HEAD"]);
|
|
158
208
|
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
209
|
+
record({ kind: "history.reset", undone: changed.length, paths: changed.slice(0, 20) }, placeOf(moduleDir));
|
|
159
210
|
return changed;
|
|
160
211
|
}
|
|
161
212
|
catch {
|
|
@@ -235,6 +286,7 @@ export function abandonWork(start, moduleName, why) {
|
|
|
235
286
|
git(moduleDir, ["reset", "-q", "--hard", commit]);
|
|
236
287
|
git(moduleDir, ["clean", "-q", "-fd"]);
|
|
237
288
|
cleanTestsBuild(moduleDir);
|
|
289
|
+
record({ kind: "history.rolledBack", why: firstLine(why, 300) }, placeOf(moduleDir));
|
|
238
290
|
reportActivity(`${moduleName}: ${why} - ${basename(moduleDir)} is back where the work started`);
|
|
239
291
|
}
|
|
240
292
|
catch {
|
|
@@ -385,6 +385,12 @@ export declare function isUpToDate(status: ModuleStatus): boolean;
|
|
|
385
385
|
export declare function describeModules(statuses: ModuleStatus[], options?: {
|
|
386
386
|
for?: "chat" | "command line";
|
|
387
387
|
}): string;
|
|
388
|
+
/**
|
|
389
|
+
* A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
|
|
390
|
+
* unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
|
|
391
|
+
* be tried.
|
|
392
|
+
*/
|
|
393
|
+
export declare function skipsReview(cases: JobCases): boolean;
|
|
388
394
|
/** How a code pulse went, for its line in the notes: the rounds it took, its test runs, what the debugger found. */
|
|
389
395
|
export declare function codeStory(result: {
|
|
390
396
|
attempts?: number;
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -5,14 +5,15 @@ import { STDLIB_ROOT } from "../plain/stdlib.js";
|
|
|
5
5
|
import { ACCEPTANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportStage } from "./activity.js";
|
|
7
7
|
import { llmChunkAgents, MAX_LINES_PER_JOB, MAX_SPECS_PER_JOB, planChunks, stepsOf } from "./chunk-plan.js";
|
|
8
|
-
import { commitNotes, recordHistory, resetToLastCommit } from "./lineage.js";
|
|
8
|
+
import { commitNotes, landWork, recordHistory, resetToLastCommit } from "./lineage.js";
|
|
9
|
+
import { firstLine, record } from "./ledger.js";
|
|
9
10
|
import { NOTES_FILE, recordPulse } from "./notes.js";
|
|
10
11
|
import { createJobRegistry, formatDuration } from "./jobs.js";
|
|
11
12
|
import { moduleLanes } from "./lanes.js";
|
|
12
|
-
import { adoptDigests, chainTree, codeTree, conformanceTarget, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
|
|
13
|
+
import { adoptDigests, chainTree, codeTree, conformanceTarget, marksRegenerateAgain, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
|
|
13
14
|
import { describeSpecDiffConformance, renderSpecDiffConformance } from "./conform.js";
|
|
14
15
|
import { conformanceDirFor } from "./conformance.js";
|
|
15
|
-
import { modulesRoot } from "./paths.js";
|
|
16
|
+
import { modulesRoot, projectRootOf } from "./paths.js";
|
|
16
17
|
import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn, stackForModule, stackPinned, withStack } from "../config.js";
|
|
17
18
|
import { describeSpecChange } from "./prompt.js";
|
|
18
19
|
import { resolveTemplateDirs } from "./paths.js";
|
|
@@ -153,7 +154,8 @@ export function functionalityCompletion(plainFile) {
|
|
|
153
154
|
codebase.forEach((entry, index) => {
|
|
154
155
|
const count = counts[index];
|
|
155
156
|
const completed = entry.frids.filter((frid) => !touched.has(`${entry.moduleName}:${frid}`));
|
|
156
|
-
|
|
157
|
+
// Tests marked to regenerate again were written against code since replaced: not conformance tested until rewritten.
|
|
158
|
+
const tested = (readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDirOf(entry.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).filter((item) => item.frid !== undefined && !marksRegenerateAgain(item) && item.markdown.trim() === specText(entry.moduleName, item.frid));
|
|
157
159
|
count.completed = completed.length;
|
|
158
160
|
count.tested = completed.filter((frid) => tested.some((item) => item.frid === frid));
|
|
159
161
|
count.conformanceTested = count.tested.length;
|
|
@@ -190,6 +192,11 @@ export function createWorkLoop(options) {
|
|
|
190
192
|
// One tick at a time: each reads the records and the spec, and decides.
|
|
191
193
|
let ticking = Promise.resolve();
|
|
192
194
|
const tipEntry = (state) => tipOf(state.chain);
|
|
195
|
+
/** Where what the loop decides for a module is recorded: its tip's project, the module and the pulse it is about. */
|
|
196
|
+
const loopPlace = (state, job) => {
|
|
197
|
+
const tip = tipEntry(state);
|
|
198
|
+
return { project: projectRootOf(tip.buildDir), module: tip.moduleName, job };
|
|
199
|
+
};
|
|
193
200
|
/** `work` with the codebase's stack chosen - the one it was asked for with - when it has one pinned. */
|
|
194
201
|
const inStack = (state, work) => (state.stack !== undefined ? withStack(state.stack.name, work) : work());
|
|
195
202
|
const keyOf = (spec) => serializeTree(codeTree(spec.tree));
|
|
@@ -521,7 +528,7 @@ export function createWorkLoop(options) {
|
|
|
521
528
|
run: async (onDelta) => {
|
|
522
529
|
// The guardrail: a conformance job tests only code that is built - the code record, read as the job starts, has every functional spec it tests as it tests it.
|
|
523
530
|
if (conformance) {
|
|
524
|
-
const testing = [...planned.cases.add, ...planned.cases.patch].map((touched) => touched.frid);
|
|
531
|
+
const testing = [...planned.cases.add, ...planned.cases.patch, ...planned.cases.again].map((touched) => touched.frid);
|
|
525
532
|
const missing = notBuilt(readRecord(tip.buildDir), planned.step.tree, testing);
|
|
526
533
|
if (missing.length > 0) {
|
|
527
534
|
const record = relative(options.cwd, join(tip.buildDir, RECORD_FILE));
|
|
@@ -680,6 +687,7 @@ export function createWorkLoop(options) {
|
|
|
680
687
|
/* a spec that does not parse halts it all the same */
|
|
681
688
|
}
|
|
682
689
|
state.halted = { why: `${why} - nothing more is built on it until its conformance tests pass`, specKey: key, pulse: pulseOf(running), ...(settled?.status === "stopped" ? {} : causeOf(settled)) };
|
|
690
|
+
record({ kind: "loop.halted", why: firstLine(state.halted.why, 300), track: "conformance" }, loopPlace(state, running.jobId));
|
|
683
691
|
}
|
|
684
692
|
}
|
|
685
693
|
else if (running !== undefined && !landed) {
|
|
@@ -699,6 +707,7 @@ export function createWorkLoop(options) {
|
|
|
699
707
|
}
|
|
700
708
|
const why = settled?.status === "stopped" ? `pulse ${running.jobId} was stopped` : `pulse ${running.jobId} ("${running.planned.title}") did not land`;
|
|
701
709
|
state.halted = { why: `${why} - it runs again once the spec changes, or the module is asked for again`, specKey: key, pulse: pulseOf(running), ...(settled?.status === "stopped" ? {} : causeOf(settled)) };
|
|
710
|
+
record({ kind: "loop.halted", why: firstLine(state.halted.why, 300), track: "code" }, loopPlace(state, running.jobId));
|
|
702
711
|
}
|
|
703
712
|
}
|
|
704
713
|
else {
|
|
@@ -865,6 +874,7 @@ export function createWorkLoop(options) {
|
|
|
865
874
|
else {
|
|
866
875
|
state.running.stopping = true;
|
|
867
876
|
state.why = `pulse ${state.running.jobId} was stopped: ${why}, which it was regenerating from`;
|
|
877
|
+
record({ kind: "loop.stopped", why: firstLine(why, 300) }, loopPlace(state, state.running.jobId));
|
|
868
878
|
reportActivity(` ${tip.moduleName}: ${state.why}`);
|
|
869
879
|
options.registry.stop(state.running.jobId, `Stopped by turri, not by you: ${why}, which it was regenerating from. What it had done is rolled back, and the spec as it is now is regenerated instead. To have a running pulse finish first and the change regenerated on top of it, set allow-to-stop-jobs: false in config.turri.yaml.`);
|
|
870
880
|
return;
|
|
@@ -1109,6 +1119,17 @@ export function createWorkLoop(options) {
|
|
|
1109
1119
|
found.add(frid);
|
|
1110
1120
|
const marked = items.map((item, index) => (frids.includes(named(item, index)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
|
|
1111
1121
|
writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
|
|
1122
|
+
// Its conformance tests were written against the code being replaced: marked with it, they are written again -
|
|
1123
|
+
// checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
|
|
1124
|
+
if (section === FUNCTIONAL_REQUIREMENTS) {
|
|
1125
|
+
const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
|
|
1126
|
+
const tests = readRecord(testsDir);
|
|
1127
|
+
const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
|
|
1128
|
+
if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
|
|
1129
|
+
const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
|
|
1130
|
+
writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1112
1133
|
recordHistory(tip.buildDir, `${module} functional spec${taken.length === 1 ? "" : "s"} ${taken.join(", ")} to regenerate again`);
|
|
1113
1134
|
}
|
|
1114
1135
|
const missing = frids.filter((frid) => !found.has(frid));
|
|
@@ -1596,10 +1617,18 @@ const defaultConformanceRunner = async (job, onDelta) => {
|
|
|
1596
1617
|
});
|
|
1597
1618
|
if (result.passed) {
|
|
1598
1619
|
writeTestsRecord(result.conformanceDir, job.planned.step.tree);
|
|
1599
|
-
|
|
1620
|
+
landWork(job.buildDir, `conformance tests: ${job.planned.title}`);
|
|
1600
1621
|
}
|
|
1601
1622
|
return { landed: result.passed, report: describeSpecDiffConformance(result), ...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}) };
|
|
1602
1623
|
};
|
|
1624
|
+
/**
|
|
1625
|
+
* A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
|
|
1626
|
+
* unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can
|
|
1627
|
+
* be tried.
|
|
1628
|
+
*/
|
|
1629
|
+
export function skipsReview(cases) {
|
|
1630
|
+
return cases.fresh && cases.add.length <= 1 && cases.patch.length === 0 && cases.again.length === 0 && cases.delete.length === 0;
|
|
1631
|
+
}
|
|
1603
1632
|
/** A job as a spec diff render, landed on its unit tests: conformance is a track of its own. */
|
|
1604
1633
|
const defaultRunner = async (job, onDelta) => {
|
|
1605
1634
|
const result = await renderSpecDiff({
|
|
@@ -1609,6 +1638,7 @@ const defaultRunner = async (job, onDelta) => {
|
|
|
1609
1638
|
applied: "already applied",
|
|
1610
1639
|
spec: job.spec,
|
|
1611
1640
|
cases: job.planned.cases,
|
|
1641
|
+
...(skipsReview(job.planned.cases) ? { unitReview: false } : {}),
|
|
1612
1642
|
...(job.model !== undefined ? { model: job.model } : {}),
|
|
1613
1643
|
...(job.steering !== undefined ? { steering: job.steering } : {}),
|
|
1614
1644
|
onDelta,
|
|
@@ -63,9 +63,11 @@ export type Chosen = (frid: string) => boolean;
|
|
|
63
63
|
* definitions and implementation reqs, with the test reqs as the spec has them now.
|
|
64
64
|
*
|
|
65
65
|
* Conformance follows the code, never goes ahead of it: a functional spec whose code is not caught up
|
|
66
|
-
* with the spec - its text in the code record is not the spec's,
|
|
67
|
-
* all,
|
|
68
|
-
* taken out only once the spec and the code have both dropped it.
|
|
66
|
+
* with the spec - its text in the code record is not the spec's, the code record does not have it at
|
|
67
|
+
* all, or has it marked to regenerate again - stays as the tests record has it, until the code lands. Its tests are
|
|
68
|
+
* taken out only once the spec and the code have both dropped it. Tests marked to regenerate again with their code
|
|
69
|
+
* (`regenerateAgain`) differ from the target once the code lands: written against the code it replaced, they are
|
|
70
|
+
* conformance work again, though the spec's text never changed.
|
|
69
71
|
*/
|
|
70
72
|
export declare function conformanceTarget(code: MarshalledPlainSource | null, tests: MarshalledPlainSource | null, spec: MarshalledPlainSource, chosen?: Chosen): MarshalledPlainSource;
|
|
71
73
|
/**
|
|
@@ -155,9 +155,11 @@ export function inheritedRecord(source, sourceModule) {
|
|
|
155
155
|
* definitions and implementation reqs, with the test reqs as the spec has them now.
|
|
156
156
|
*
|
|
157
157
|
* Conformance follows the code, never goes ahead of it: a functional spec whose code is not caught up
|
|
158
|
-
* with the spec - its text in the code record is not the spec's,
|
|
159
|
-
* all,
|
|
160
|
-
* taken out only once the spec and the code have both dropped it.
|
|
158
|
+
* with the spec - its text in the code record is not the spec's, the code record does not have it at
|
|
159
|
+
* all, or has it marked to regenerate again - stays as the tests record has it, until the code lands. Its tests are
|
|
160
|
+
* taken out only once the spec and the code have both dropped it. Tests marked to regenerate again with their code
|
|
161
|
+
* (`regenerateAgain`) differ from the target once the code lands: written against the code it replaced, they are
|
|
162
|
+
* conformance work again, though the spec's text never changed.
|
|
161
163
|
*/
|
|
162
164
|
export function conformanceTarget(code, tests, spec, chosen = () => true) {
|
|
163
165
|
const built = itemsOf(code, FUNCTIONAL_REQUIREMENTS);
|
|
@@ -168,7 +170,7 @@ export function conformanceTarget(code, tests, spec, chosen = () => true) {
|
|
|
168
170
|
for (let index = 0; index < Math.max(built.length, now.length); index += 1) {
|
|
169
171
|
const frid = String(index + 1);
|
|
170
172
|
const current = now[index];
|
|
171
|
-
if (current !== undefined && built[index]?.markdown.trim() === current.markdown.trim()) {
|
|
173
|
+
if (current !== undefined && built[index]?.markdown.trim() === current.markdown.trim() && !marksRegenerateAgain(built[index])) {
|
|
172
174
|
if (chosen(frid))
|
|
173
175
|
specs.push({ ...current, frid });
|
|
174
176
|
continue;
|
|
@@ -11,7 +11,8 @@ import { debugFailure, diagnosisText } from "./debugger.js";
|
|
|
11
11
|
import { codebaseMap, diffFiles, digestFiles, wroteNothing } from "./codebase.js";
|
|
12
12
|
import { confinedTools } from "./confine.js";
|
|
13
13
|
import { buildSpecDiffPrompt, describeSpecChange, reviewedSpec, sectionModules } from "./prompt.js";
|
|
14
|
-
import { abandonWork,
|
|
14
|
+
import { abandonWork, landWork, startWork } from "./lineage.js";
|
|
15
|
+
import { healing, record, signatureHash } from "./ledger.js";
|
|
15
16
|
import { createWorkLedger, SUBMIT_WORK_NOTE } from "./submit-work.js";
|
|
16
17
|
import { CODER_COMPACTION } from "./compaction.js";
|
|
17
18
|
import { gapMark, gapsSince, NOTES_TURN, notesForSession, passedOutcome, recordNote, withNotes } from "./notes.js";
|
|
@@ -193,7 +194,7 @@ export async function renderSpecDiff(options) {
|
|
|
193
194
|
if (builtBefore !== null && sameSpec(builtBefore, tree) && !dryRun) {
|
|
194
195
|
stage("the specs were only restructured - nothing to render; recording the spec as it is now laid out");
|
|
195
196
|
writeCodeRecord(tip.buildDir, tree);
|
|
196
|
-
|
|
197
|
+
landWork(tip.buildDir, `spec restructured: ${summarizeDiff(options.diff)}`, options.diff);
|
|
197
198
|
return {
|
|
198
199
|
moduleName,
|
|
199
200
|
buildDir: tip.buildDir,
|
|
@@ -440,7 +441,7 @@ export async function renderSpecDiff(options) {
|
|
|
440
441
|
: timedOut && seen < STUCK_ROUNDS
|
|
441
442
|
? `the unit testing script did not finish in ${Math.round(verdict.run.timeoutMs / 1000)}s - a debugger works out whether something hangs`
|
|
442
443
|
: `the same failure ${seen} times - a debugger looks at it with fresh eyes`);
|
|
443
|
-
const found = await debugFailure({
|
|
444
|
+
const found = await healing("debugger", () => debugFailure({
|
|
444
445
|
buildDir,
|
|
445
446
|
failure: verdict.run.output,
|
|
446
447
|
times,
|
|
@@ -450,7 +451,7 @@ export async function renderSpecDiff(options) {
|
|
|
450
451
|
label: `${moduleName} spec change`,
|
|
451
452
|
...(debuggerModel !== undefined ? { model: debuggerModel } : {}),
|
|
452
453
|
onDelta: options.onDelta,
|
|
453
|
-
})
|
|
454
|
+
}), (finding) => finding?.rootCause);
|
|
454
455
|
// Nothing hangs: the suite needs more time than a test run may take, and no agent can give it that.
|
|
455
456
|
if (found?.tooSlow === true) {
|
|
456
457
|
stage(`the unit testing script needs more than the ${Math.round(verdict.run.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
@@ -477,6 +478,7 @@ export async function renderSpecDiff(options) {
|
|
|
477
478
|
].join("\n");
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
record({ kind: "gate.round", gate: "unit tests", round: followUp + 1, of: MAX_FOLLOW_UPS, ...(key !== undefined ? { signature: signatureHash(key) } : {}) });
|
|
480
482
|
stage(`${verdict.staticOnly ? "static checks" : "unit tests"} not passing - fixing (round ${followUp + 1} of ${MAX_FOLLOW_UPS})`);
|
|
481
483
|
response = await ask(executor, prompt, options.onDelta);
|
|
482
484
|
continue;
|
|
@@ -554,7 +556,7 @@ export async function renderSpecDiff(options) {
|
|
|
554
556
|
stage("recording what the code is built from, and committing it");
|
|
555
557
|
// Steered, it is built from the spec as it was steered to - the record the loop moved with it.
|
|
556
558
|
writeCodeRecord(buildDir, steering?.target ?? tree);
|
|
557
|
-
|
|
559
|
+
landWork(buildDir, `spec diff: ${summarizeDiff(options.diff)}`, options.diff);
|
|
558
560
|
};
|
|
559
561
|
if (!unitPassed) {
|
|
560
562
|
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
@@ -6,7 +6,8 @@ import { reportActivity, reportStage, unitOf } from "./activity.js";
|
|
|
6
6
|
import { confinedTools } from "./confine.js";
|
|
7
7
|
import { projectRootOf } from "./paths.js";
|
|
8
8
|
import { buildRegressionFixPrompt, sectionModules } from "./prompt.js";
|
|
9
|
-
import { abandonWork,
|
|
9
|
+
import { abandonWork, landWork, markWork } from "./lineage.js";
|
|
10
|
+
import { record } from "./ledger.js";
|
|
10
11
|
import { missingToolchainError } from "./environment-error.js";
|
|
11
12
|
import { workspaceBlock } from "./workspace.js";
|
|
12
13
|
import { CODER_COMPACTION } from "./compaction.js";
|
|
@@ -154,13 +155,14 @@ export async function ensureRegressionPasses(input) {
|
|
|
154
155
|
if (passing) {
|
|
155
156
|
reportStage(`${unitOf(input.moduleName)}: the whole conformance suite passes, and the unit tests with it${repairs.length > 0 ? ` after ${repairs.length} repair${repairs.length === 1 ? "" : "s"}` : ""}`);
|
|
156
157
|
if (repairs.length > 0) {
|
|
157
|
-
|
|
158
|
+
landWork(input.buildDir, `regression ${input.moduleName} (pass after ${repairs.length} repair(s))`);
|
|
158
159
|
}
|
|
159
160
|
return { passed: true, runs, repairs, command };
|
|
160
161
|
}
|
|
161
162
|
if (round === maxRounds)
|
|
162
163
|
break;
|
|
163
164
|
input.onEvent?.({ type: "repair", frids: input.frids });
|
|
165
|
+
record({ kind: "gate.round", gate: "regression", round: round + 1, of: maxRounds });
|
|
164
166
|
reportStage(`${unitOf(input.moduleName)}: an earlier functionality broke - repairing (round ${round + 1} of ${maxRounds})`);
|
|
165
167
|
repairs.push(await repairRegression({
|
|
166
168
|
testsDirs: dirs,
|
|
@@ -17,6 +17,8 @@ import type { TranscriptStyle } from "./transcript-view.ts";
|
|
|
17
17
|
export interface SidePanelOptions {
|
|
18
18
|
/** Every job, oldest first. */
|
|
19
19
|
jobs: () => Job[];
|
|
20
|
+
/** Earlier sessions' pulses, from the ledger: listed under this session's, in a section of their own. */
|
|
21
|
+
earlier?: () => Job[];
|
|
20
22
|
/** The regeneration's pages. Not given: the pulse list alone. */
|
|
21
23
|
map?: () => MarkedPage[];
|
|
22
24
|
/** Rows the panel may use. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
import { createJobsTreeState, jobsTreeLines, jobsTreeRows, keepCursorInView, scrollTree, treeKey } from "./jobs-tree.js";
|
|
2
|
+
import { createJobsTreeState, jobsTreeLines, jobsTreeRows, keepCursorInView, scrollTree, sectionOf, treeKey } from "./jobs-tree.js";
|
|
3
3
|
import { columnHeadings, createSpecMapState, DISCUSS_KEY, legendLines, minimap, shownPage, specMapHeader, specMapLines, switchTab, tabBar } from "./spec-map-view.js";
|
|
4
4
|
export function createSidePanel(options) {
|
|
5
5
|
const style = options.style;
|
|
@@ -71,7 +71,8 @@ export function createSidePanel(options) {
|
|
|
71
71
|
const bodyHeight = Math.max(3, rows - top.length - 2);
|
|
72
72
|
lastBodyHeight = bodyHeight;
|
|
73
73
|
// The tree scrolls to keep the cursor in sight, and only that far.
|
|
74
|
-
|
|
74
|
+
// This session's pulses, and under them the earlier sessions' the ledger has.
|
|
75
|
+
const tree_ = jobsTreeLines([...options.jobs(), ...(options.earlier?.() ?? [])], tree, width, style);
|
|
75
76
|
drawnTree = { rows: tree_.rows, rowStarts: tree_.rowStarts, lineCount: tree_.lines.length };
|
|
76
77
|
keepCursorInView(tree, tree_.rowStarts, tree_.lines.length, bodyHeight);
|
|
77
78
|
const visible = tree_.lines.slice(tree.top, tree.top + bodyHeight);
|
|
@@ -132,7 +133,7 @@ export function createSidePanel(options) {
|
|
|
132
133
|
const job = jobs.find((entry) => entry.id === id);
|
|
133
134
|
if (job === undefined)
|
|
134
135
|
return;
|
|
135
|
-
const section = job
|
|
136
|
+
const section = sectionOf(job);
|
|
136
137
|
tree.open[section] = true;
|
|
137
138
|
const at = jobsTreeRows(jobs, tree).findIndex((entry) => entry.kind === "job" && entry.job.id === id);
|
|
138
139
|
if (at !== -1)
|
|
@@ -8,7 +8,7 @@ import { conformanceDirFor } from "./conformance.js";
|
|
|
8
8
|
import { chainSpec } from "./loop.js";
|
|
9
9
|
import { modulesRoot, projectDirOf } from "./paths.js";
|
|
10
10
|
import { HISTORY_DIR } from "./lineage.js";
|
|
11
|
-
import { codeTree, readRecord, RECORD_FILE } from "./records.js";
|
|
11
|
+
import { codeTree, marksRegenerateAgain, readRecord, RECORD_FILE } from "./records.js";
|
|
12
12
|
import { specFilesOf } from "./regenerate.js";
|
|
13
13
|
import { builtTips, resolveRequiresChain } from "./requires.js";
|
|
14
14
|
import { changesBetween, layoutOf, serializeTree } from "./spec-steps.js";
|
|
@@ -115,17 +115,20 @@ export function pageItems(input) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
// Conformance tests: of the functional specs, those the tests record has as they read now, as they read before, or taken out.
|
|
118
|
-
|
|
118
|
+
// Tests marked to regenerate again were written against code since replaced: written for an earlier build, not now.
|
|
119
|
+
const testedItems = itemsOf(input.tested ?? null, FUNCTIONAL_REQUIREMENTS);
|
|
120
|
+
const tested = new Set(testedItems.filter((item) => !marksRegenerateAgain(item)).map((item) => item.markdown.trim()));
|
|
121
|
+
const replaced = new Set(testedItems.filter((item) => marksRegenerateAgain(item)).map((item) => item.markdown.trim()));
|
|
119
122
|
for (const item of items) {
|
|
120
|
-
if (item.section !== FUNCTIONAL_REQUIREMENTS ||
|
|
123
|
+
if (item.section !== FUNCTIONAL_REQUIREMENTS || testedItems.length === 0)
|
|
121
124
|
continue;
|
|
122
125
|
if (item.state === "removed") {
|
|
123
|
-
if (tested.has(item.text.trim()))
|
|
126
|
+
if (tested.has(item.text.trim()) || replaced.has(item.text.trim()))
|
|
124
127
|
item.tests = "removed";
|
|
125
128
|
}
|
|
126
129
|
else if (tested.has(item.text.trim()))
|
|
127
130
|
item.tests = "now";
|
|
128
|
-
else if (item.was !== undefined && tested.has(item.was.trim()))
|
|
131
|
+
else if (replaced.has(item.text.trim()) || (item.was !== undefined && (tested.has(item.was.trim()) || replaced.has(item.was.trim()))))
|
|
129
132
|
item.tests = "older";
|
|
130
133
|
}
|
|
131
134
|
return keyed(items);
|
|
@@ -4,6 +4,7 @@ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync
|
|
|
4
4
|
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { CONFIG_FILE, CONFIG_KEYS, projectConfigFile, readProjectConfig, stackConfigFile, stackConfigFileIn, stackOf, stacksOf } from "../config.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportNote } from "./activity.js";
|
|
7
|
+
import { record, signatureHash } from "./ledger.js";
|
|
7
8
|
import { ensureInModulesScratch, modulesScratchDir } from "./paths.js";
|
|
8
9
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
@@ -361,7 +362,7 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
361
362
|
const noted = withScriptLineNote(text, script);
|
|
362
363
|
// Its failures, counts and end are what the agent is shown; the whole is kept, a read_output away.
|
|
363
364
|
const log = keepTestOutput(logDir, basename(script), noted);
|
|
364
|
-
|
|
365
|
+
const run = {
|
|
365
366
|
command: shown(script),
|
|
366
367
|
exitCode: timedOut === true ? 124 : exitCode,
|
|
367
368
|
output: shownTestOutput(noted, log),
|
|
@@ -370,6 +371,16 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
370
371
|
elapsedMs: Date.now() - started,
|
|
371
372
|
...(log !== undefined ? { log } : {}),
|
|
372
373
|
};
|
|
374
|
+
record({
|
|
375
|
+
kind: "script.run",
|
|
376
|
+
script: basename(script),
|
|
377
|
+
exit: run.exitCode,
|
|
378
|
+
ms: run.elapsedMs,
|
|
379
|
+
timedOut: run.timedOut === true,
|
|
380
|
+
inPlace: options.named === undefined,
|
|
381
|
+
...(run.exitCode !== 0 ? { signature: signatureHash(failureSignature(run)) } : {}),
|
|
382
|
+
});
|
|
383
|
+
return run;
|
|
373
384
|
}
|
|
374
385
|
/** What bash says when a line of the script itself cannot run: a command or file that is not there, or not runnable. */
|
|
375
386
|
const OWN_LINE_FAILURE = /: (No such file or directory|command not found|Permission denied)$/;
|
|
@@ -3,6 +3,7 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { ask, createExecutor } from "../pi.js";
|
|
5
5
|
import { reportActivity } from "./activity.js";
|
|
6
|
+
import { record } from "./ledger.js";
|
|
6
7
|
import { diffFiles, digestFiles } from "./codebase.js";
|
|
7
8
|
import { confinedTools } from "./confine.js";
|
|
8
9
|
import { REVIEWER_CALLS, reviewKind } from "./conformance-agents.js";
|
|
@@ -288,6 +289,7 @@ async function reviewLoop(options, reviewer) {
|
|
|
288
289
|
letGo = [];
|
|
289
290
|
options.stage(`reviewing the code and its unit tests (review ${reviews + 1}): does the code do what the spec says, and does each test fail when that breaks`);
|
|
290
291
|
const review = await reviewer.review(request);
|
|
292
|
+
record({ kind: "review.verdict", reviewer: "unit", review: reviews + 1, pass: review.pass, problems: review.pass ? 0 : review.problems.length });
|
|
291
293
|
if (review.pass) {
|
|
292
294
|
options.stage("the review has nothing left to improve in the code or its unit tests");
|
|
293
295
|
return waived.length > 0 ? { passed: true, waived, reviews: reviews + 1 } : { passed: true, reviews: reviews + 1 };
|
|
@@ -316,6 +318,7 @@ async function reviewLoop(options, reviewer) {
|
|
|
316
318
|
});
|
|
317
319
|
// No ruling: the review stands, as it did before there was an arbiter.
|
|
318
320
|
if (ruling !== undefined) {
|
|
321
|
+
record({ kind: "arbiter.ruling", upheld: ruling.upheld.length, dismissed: ruling.dismissed.length });
|
|
319
322
|
waived.push(...ruling.dismissed.map((entry) => entry.problem));
|
|
320
323
|
if (ruling.dismissed.length > 0)
|
|
321
324
|
recordNote(options.buildDir, "pitfalls", dismissedNote(ruling, "unit test"), `${options.label} unit tests`);
|
package/dist/run-report.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Job } from "./regenerate/jobs.ts";
|
|
2
2
|
/**
|
|
3
3
|
* A headless run's report: one JSON file, `turri regenerate --report <file>`, saying what the run did - how it ended,
|
|
4
4
|
* what it spent, which pulses landed, how many functional specs are completed - for a benchmark or CI to read rather
|
package/dist/run-report.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { turriIdentity } from "./environment.js";
|
|
4
4
|
import { onModelEvent } from "./regenerate/activity.js";
|
|
5
|
+
import { firstRegeneration } from "./regenerate/jobs.js";
|
|
5
6
|
/**
|
|
6
7
|
* A headless run's report: one JSON file, `turri regenerate --report <file>`, saying what the run did - how it ended,
|
|
7
8
|
* what it spent, which pulses landed, how many functional specs are completed - for a benchmark or CI to read rather
|
|
@@ -177,7 +178,12 @@ export function startRunReport(options) {
|
|
|
177
178
|
};
|
|
178
179
|
});
|
|
179
180
|
const ran = list.filter((pulse) => pulse.status !== "queued");
|
|
181
|
+
const first = firstRegeneration(jobs, startedAt);
|
|
182
|
+
const seconds = (ms) => (ms !== undefined ? Math.round(ms / 100) / 10 : null);
|
|
180
183
|
return {
|
|
184
|
+
// The time to first regeneration - the first code pulse started - and to the first one landed, from the run's start.
|
|
185
|
+
first_started_seconds: seconds(first.started),
|
|
186
|
+
first_landed_seconds: seconds(first.landed),
|
|
181
187
|
counts: {
|
|
182
188
|
total: ran.length,
|
|
183
189
|
landed: ran.filter((pulse) => pulse.landed).length,
|
|
@@ -20,6 +20,6 @@ export declare function selfInvocation(command: string): string | undefined;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function askedForCommandLine(message: string): boolean;
|
|
22
22
|
/** What the agent is told when the call is refused. */
|
|
23
|
-
export declare function refusal(what: string): string;
|
|
23
|
+
export declare function refusal(what: string, surface?: string): string;
|
|
24
24
|
/** The instruction that goes into the conversation's system prompt. */
|
|
25
25
|
export declare const TOOLS_NOT_CLI: string;
|
package/dist/self-invocation.js
CHANGED
|
@@ -88,12 +88,13 @@ export function askedForCommandLine(message) {
|
|
|
88
88
|
/(^|\s)pi\s+-p\b/.test(text));
|
|
89
89
|
}
|
|
90
90
|
/** What the agent is told when the call is refused. */
|
|
91
|
-
export function refusal(what) {
|
|
91
|
+
export function refusal(what, surface) {
|
|
92
92
|
const instead = what === "pi"
|
|
93
93
|
? "Do not start another agent: do the work here, with this conversation's tools."
|
|
94
94
|
: `Use ${TOOL_FOR[what] ?? "the matching turri tool"} instead.`;
|
|
95
95
|
return (`Not run: that starts turri${what === "pi" ? "'s agent" : ` ${what}`} again from the shell - a separate, headless ` +
|
|
96
|
-
|
|
96
|
+
// In a host there is no panel: the person watches the pulses where its surface says.
|
|
97
|
+
(surface !== undefined ? "run outside this conversation and its pulses, where the person can neither watch it nor halt it. " : "run outside this conversation, its jobs, the panel and /halt. ") +
|
|
97
98
|
`${instead} Run turri from the command line only when the user asks for that.`);
|
|
98
99
|
}
|
|
99
100
|
/** The instruction that goes into the conversation's system prompt. */
|
package/dist/skill-progress.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* brood ✓ intent ━━━━ ⠋ stack ──── ○ testing ──── ○ product phase 2 of 4
|
|
7
7
|
* stack - the language, frameworks, storage and architecture
|
|
8
8
|
*
|
|
9
|
-
* The second line says what the phase it is in does: a phase's name alone - `
|
|
9
|
+
* The second line says what the phase it is in does: a phase's name alone - `skeleton` - says little to someone
|
|
10
10
|
* who has not read the skill.
|
|
11
11
|
*
|
|
12
12
|
* A skill of one phase, or none, has nothing to be partway through: its line is the spinner and its name alone.
|
package/dist/skill.d.ts
CHANGED
|
@@ -85,6 +85,11 @@ export interface Skill {
|
|
|
85
85
|
* last answer is written with `write_spec_diff`, and changes the agent decides itself need no answer before them.
|
|
86
86
|
*/
|
|
87
87
|
diffPerAnswer: string[];
|
|
88
|
+
/**
|
|
89
|
+
* Started only by the person, with its slash command - `/brood` - never by the model on its own
|
|
90
|
+
* (`metadata.command-only: true`): cultivate runs it only when the person has just typed its command.
|
|
91
|
+
*/
|
|
92
|
+
commandOnly: boolean;
|
|
88
93
|
}
|
|
89
94
|
export declare function loadSkill(skillDir: string): Skill;
|
|
90
95
|
export declare function readRubric(skill: Skill, review: {
|
package/dist/skill.js
CHANGED
package/dist/skills.d.ts
CHANGED
|
@@ -28,3 +28,17 @@ export declare function discoverSkills(packageRoot: string, projectDir: string |
|
|
|
28
28
|
export declare function catalogueLine(skill: FoundSkill): string;
|
|
29
29
|
/** What is said under the list about the skills it leaves out. */
|
|
30
30
|
export declare function leftOutNote(catalogue: SkillCatalogue): string;
|
|
31
|
+
/**
|
|
32
|
+
* Which skills cultivate may start now. A skill marked command-only (`/brood`) waits for the person to type its command:
|
|
33
|
+
* the command lets it start once, and the run starting uses that up - the model cannot start it again later on its own.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createCommandGate(): {
|
|
36
|
+
/** The person typed the skill's command. */
|
|
37
|
+
typed(name: string): void;
|
|
38
|
+
/** Whether cultivate may start it now. */
|
|
39
|
+
mayStart(skill: Pick<FoundSkill, "name" | "commandOnly">): boolean;
|
|
40
|
+
/** Its run started: the command is used up. */
|
|
41
|
+
started(name: string): void;
|
|
42
|
+
};
|
|
43
|
+
/** What cultivate says of a command-only skill the person did not start. */
|
|
44
|
+
export declare function startedByCommandOnly(name: string): string;
|