@turri-dev/cli 0.1.23 → 0.1.24-next.2b2f203

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.
@@ -51,11 +51,17 @@ export declare function sessionLoop(session: TurriCore): WorkLoop;
51
51
  /**
52
52
  * What a slash command showed the person: kept in the chat, where they read it, and put in the conversation as well -
53
53
  * unseen there - so that when they speak of it next ("fix it"), the agent knows what they saw. A command runs outside
54
- * the agent's turns, and a notification is gone in seconds and reaches no model.
54
+ * the agent's turns, and a notification is gone in seconds and reaches no model. Given `react`, the agent is started on
55
+ * it too - at once when it is idle, else once its turn ends - told what to do with it.
55
56
  */
56
57
  export declare function shownAfterCommand(pi: ExtensionAPI, ctx: ExtensionContext, command: string, text: string, level: NonNullable<CommandOutcome["level"]>,
57
58
  /** The command is in the chat already, above it (echoCommand): the text alone. */
58
- echoed?: boolean): void;
59
+ echoed?: boolean, react?: string): void;
60
+ /**
61
+ * What the agent does once /regenerate has started pulses: a command's output alone leaves the chat silent, and the
62
+ * person wondering whether to wait. It says what runs, and that they need not - and starts nothing itself.
63
+ */
64
+ export declare const COMMAND_STARTED_REACTION: string;
59
65
  export declare const COMMAND_OUTCOME = "turri_command_outcome";
60
66
  interface CommandOutcome {
61
67
  command: string;
package/dist/extension.js CHANGED
@@ -949,7 +949,7 @@ function registerRegenerate(pi, cwd, jobs) {
949
949
  // by the planner: one functional spec at a time, a few together or the whole module in one, as it judges. What
950
950
  // follows the module is guidance for that plan, in the person's words.
951
951
  pi.registerCommand("regenerate", {
952
- description: "Regenerate a module from its spec as it is now, in pulses the planner cuts - named functionalities though they did not change, or all of it from scratch, its whole chain too when asked: /regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]",
952
+ description: "Regenerate a module from its ***plain as it is now, in pulses the planner cuts - named functionalities though they did not change, or all of it from scratch, its whole chain too when asked: /regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]",
953
953
  handler: async (args, ctx) => {
954
954
  const command = `/regenerate ${args.trim()}`.trim();
955
955
  await echoCommand(pi, command);
@@ -1000,8 +1000,8 @@ function registerRegenerate(pi, cwd, jobs) {
1000
1000
  }
1001
1001
  if (scratch && ctx.hasUI) {
1002
1002
  const agreed = await ctx.ui.confirm(chain ? `Regenerate ${module} and its whole chain from scratch?` : `Regenerate ${module} from scratch?`, chain
1003
- ? `The code and conformance tests of ${module} and of every module it requires are deleted - committed first, so /lineage keeps them - and the whole chain is regenerated from its spec, from the bottom.`
1004
- : `Its code and conformance tests are deleted - committed first, so /lineage ${module} keeps them - and the whole module is regenerated from its spec.`);
1003
+ ? `The code and conformance tests of ${module} and of every module it requires are deleted - committed first, so /lineage keeps them - and the whole chain is regenerated from its ***plain, from the bottom.`
1004
+ : `Its code and conformance tests are deleted - committed first, so /lineage ${module} keeps them - and the whole module is regenerated from its ***plain.`);
1005
1005
  if (!agreed)
1006
1006
  return;
1007
1007
  }
@@ -1028,10 +1028,12 @@ function registerRegenerate(pi, cwd, jobs) {
1028
1028
  ? loop.regenerateAgain(target, again, request)
1029
1029
  : loop.bringUp(target, request));
1030
1030
  // Nothing to do: say how to build it again anyway.
1031
- const hint = statuses.length > 0 && statuses.every(isUpToDate)
1031
+ const upToDate = statuses.length > 0 && statuses.every(isUpToDate);
1032
+ const hint = upToDate
1032
1033
  ? `\nTo regenerate parts of it anyway, name their functionalities - /regenerate ${basename(target)} 2.1 - or start over with /regenerate ${basename(target)} from scratch.`
1033
1034
  : "";
1034
- say(describeModules(statuses) + hint, "info");
1035
+ // Pulses started: the agent says so, and that the person need not wait - a plan alone leaves the chat silent.
1036
+ shownAfterCommand(pi, ctx, command, describeModules(statuses) + hint, "info", true, upToDate ? undefined : COMMAND_STARTED_REACTION);
1035
1037
  }
1036
1038
  catch (error) {
1037
1039
  say(error instanceof Error ? error.message : String(error), "error");
@@ -1045,11 +1047,12 @@ function registerRegenerate(pi, cwd, jobs) {
1045
1047
  /**
1046
1048
  * What a slash command showed the person: kept in the chat, where they read it, and put in the conversation as well -
1047
1049
  * unseen there - so that when they speak of it next ("fix it"), the agent knows what they saw. A command runs outside
1048
- * the agent's turns, and a notification is gone in seconds and reaches no model.
1050
+ * the agent's turns, and a notification is gone in seconds and reaches no model. Given `react`, the agent is started on
1051
+ * it too - at once when it is idle, else once its turn ends - told what to do with it.
1049
1052
  */
1050
1053
  export function shownAfterCommand(pi, ctx, command, text, level,
1051
1054
  /** The command is in the chat already, above it (echoCommand): the text alone. */
1052
- echoed = false) {
1055
+ echoed = false, react) {
1053
1056
  try {
1054
1057
  pi.appendEntry(COMMAND_OUTCOME, { command, text, level, ...(echoed ? { echoed } : {}) });
1055
1058
  }
@@ -1058,8 +1061,19 @@ echoed = false) {
1058
1061
  if (ctx.hasUI)
1059
1062
  ctx.ui.notify(text, level);
1060
1063
  }
1061
- void pi.sendMessage({ customType: COMMAND_OUTCOME, content: `The person ran ${command}, and was shown:\n${text}`, display: false });
1064
+ const content = `The person ran ${command}, and was shown:\n${text}${react !== undefined ? `\n\n${react}` : ""}`;
1065
+ if (react === undefined)
1066
+ void pi.sendMessage({ customType: COMMAND_OUTCOME, content, display: false });
1067
+ else
1068
+ void pi.sendMessage({ customType: COMMAND_OUTCOME, content, display: false }, { triggerTurn: true, deliverAs: "followUp" });
1062
1069
  }
1070
+ /**
1071
+ * What the agent does once /regenerate has started pulses: a command's output alone leaves the chat silent, and the
1072
+ * person wondering whether to wait. It says what runs, and that they need not - and starts nothing itself.
1073
+ */
1074
+ export const COMMAND_STARTED_REACTION = "My pulses are regenerating from the person's /regenerate, in the background. Tell the person, in a line or two, what " +
1075
+ "runs and what is planned - and that the two of us can go on with the ***plain, or anything else, meanwhile: they need " +
1076
+ "not wait for it. Start nothing for it: no regenerate, no check, no release - its end is told when it comes.";
1063
1077
  export const COMMAND_OUTCOME = "turri_command_outcome";
1064
1078
  /** Longer than a frame of the TUI's (16ms): an entry appended before it is drawn by its end. */
1065
1079
  const ONE_FRAME_MS = 20;
@@ -215,7 +215,7 @@ export function jobPanelLines(jobs, options = {}) {
215
215
  if (lines.length > 0 && running.length > 0)
216
216
  lines.push("");
217
217
  lines.push(`${spinner(now)} ${job.module ?? job.summary} · planning the pulses · ${formatElapsed(now - job.planning.since)}`);
218
- lines.push(" the planner is cutting the work into pulses - the first starts once it is done");
218
+ lines.push(" working out what to regenerate and cutting it into pulses - the first starts once it is done");
219
219
  }
220
220
  // Always a line of help: how to see more here, and how to inspect the jobs in the side panel.
221
221
  if (options.hints === false) {
@@ -346,7 +346,7 @@ export declare function createWorkLoop(options: {
346
346
  dryRun?: boolean;
347
347
  }): WorkLoop;
348
348
  /** Why nothing runs in a codebase whose code - and conformance tests, where they are wanted - is built from its spec as it is. */
349
- export declare const UP_TO_DATE = "built from the spec as it is";
349
+ export declare const UP_TO_DATE = "built from the ***plain as it is";
350
350
  /** The codebases the loop looks after, as the person reads them: what runs, what is planned after it, and why nothing does. */
351
351
  /**
352
352
  * The coding agent's task for a dry run's first job, built as the render builds it - and all it did rolled back. Not for
@@ -780,6 +780,28 @@ export function createWorkLoop(options) {
780
780
  };
781
781
  /** The files asked for, each with how: what the loop brings up, worked out again each look. */
782
782
  const asked = new Map();
783
+ /**
784
+ * The modules asked for whose plan is not made yet - their code being deleted, what is left being worked out, the
785
+ * planner not started - by name, with since when: each shows as being planned from the moment it is asked for, not
786
+ * only once the planner runs, so the person never looks at nothing.
787
+ */
788
+ const preparing = new Map();
789
+ const preparingFor = async (plainFile, work) => {
790
+ const name = basename(plainFile).replace(/\.plain$/, "");
791
+ if (preparing.has(name))
792
+ return work();
793
+ preparing.set(name, { since: Date.now(), plainFile: resolve(options.cwd, plainFile) });
794
+ options.registry.notify?.();
795
+ // A frame to draw it in before what follows: deleting a codebase and committing it hold the event loop.
796
+ await new Promise((resolve) => setTimeout(resolve, 20));
797
+ try {
798
+ return await work();
799
+ }
800
+ finally {
801
+ preparing.delete(name);
802
+ options.registry.notify?.();
803
+ }
804
+ };
783
805
  /** The codebases the requests bring up now - where each module's code lives, and what is to be built on it. */
784
806
  const active = () => {
785
807
  const states = new Set();
@@ -973,14 +995,28 @@ export function createWorkLoop(options) {
973
995
  startJob(state, spec, from, state.plan.jobs[0]);
974
996
  };
975
997
  const statusOf = (state) => describe(state).status;
998
+ /** The modules asked for that the loop holds nothing of yet - turri just opened, say: their code being planned all the same. */
999
+ const preparingAlone = () => {
1000
+ const held = new Set(order.map((key) => tipEntry(modules.get(key)).moduleName));
1001
+ return [...preparing]
1002
+ .filter(([name]) => !held.has(name))
1003
+ .map(([name, { since, plainFile }]) => ({
1004
+ module: name,
1005
+ plainFile,
1006
+ planned: [{ title: "the work", cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false }, track: "code", planning: true }],
1007
+ since,
1008
+ }));
1009
+ };
976
1010
  /** A codebase's status, and beside each job it lists as planned, the planned job it is - none for one being planned or scheduled. */
977
1011
  const describe = (state) => inStack(state, () => describeIn(state));
978
1012
  const describeIn = (state) => {
979
1013
  // What runs next: the code's jobs, then the conformance tests' - the module's, once its code is built - the running job first.
980
1014
  const running = state.running?.planned;
981
1015
  const own = (jobs) => (jobs ?? []).filter((job) => job !== running);
982
- // A track being planned again shows as that, not as its plan before.
983
- const replanning = state.running === undefined ? state.planning : undefined;
1016
+ // A track being planned again shows as that, not as its plan before - and a module asked for, from that moment, as
1017
+ // its code being planned: its code deleted, what is left worked out, before the planner runs.
1018
+ const planningTrack = state.running === undefined ? (state.planning ?? (preparing.has(tipEntry(state).moduleName) ? "code" : undefined)) : undefined;
1019
+ const replanning = planningTrack;
984
1020
  const jobs = [
985
1021
  ...(running !== undefined ? [running] : []),
986
1022
  ...(replanning === "code" ? [] : own(state.plan?.jobs)),
@@ -994,18 +1030,18 @@ export function createWorkLoop(options) {
994
1030
  : [{ title: group.specs.map((touched) => `fs ${touched.frid}`).join(", "), cases: { fresh: false, add: group.specs, patch: [], again: [], delete: [], mechanical: false }, track: "conformance", waitsFor: group.unbuilt }]);
995
1031
  // Being planned, nothing running: what is next - for the conformance tests, the groups chosen that it plans - not nothing.
996
1032
  const together = (state.together ?? []).flat().map((touched) => `fs ${touched.frid}`);
997
- const planningNow = state.running === undefined && state.planning !== undefined
1033
+ const planningNow = planningTrack !== undefined
998
1034
  ? [
999
1035
  {
1000
- title: state.planning === "conformance" ? (together.length > 0 ? together.join(", ") : "the conformance tests") : "the work",
1036
+ title: planningTrack === "conformance" ? (together.length > 0 ? together.join(", ") : "the conformance tests") : "the work",
1001
1037
  cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false },
1002
- track: state.planning,
1038
+ track: planningTrack,
1003
1039
  planning: true,
1004
1040
  },
1005
1041
  ]
1006
1042
  : [];
1007
1043
  // The module's conformance tests, wanted for all of it while code is still to build: planned once it is built, shown now.
1008
- const codeToBuild = lastCode !== -1 || (state.running === undefined && state.planning === "code");
1044
+ const codeToBuild = lastCode !== -1 || planningTrack === "code";
1009
1045
  const testReqs = (state.seen?.tree[TEST_REQUIREMENTS] ?? []).length > 0;
1010
1046
  const plainFile = tipEntry(state).plainFile;
1011
1047
  const wholeModule = (state.conformance !== undefined && state.conformance.chosen === undefined) || (configConformance(plainFile) && !configChosen(plainFile));
@@ -1073,70 +1109,76 @@ export function createWorkLoop(options) {
1073
1109
  };
1074
1110
  const loop = {
1075
1111
  async bringUp(plainFile, given = {}) {
1076
- const { specChange, ...request } = given;
1077
- const files = codebasesOf(resolve(options.cwd, plainFile));
1078
- for (const file of files)
1079
- asked.set(file, { ...asked.get(file), ...request });
1080
- // Asked for, it is built with its stack - the one the call names, its own, or the one chosen now - pinned, whatever
1081
- // is chosen later; a spec change keeps the one it was asked for with.
1082
- const pin = { keep: specChange === true };
1083
- const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request, pin)))));
1084
- // Asked again, a halted codebase is tried again - one the person halted too, unless only its spec changed.
1085
- for (const state of states) {
1086
- state.halted = undefined;
1087
- if (specChange !== true)
1088
- state.held = undefined;
1089
- }
1090
- await tick();
1091
- return [...new Set(states)].map(statusOf);
1112
+ const now = async () => {
1113
+ const { specChange, ...request } = given;
1114
+ const files = codebasesOf(resolve(options.cwd, plainFile));
1115
+ for (const file of files)
1116
+ asked.set(file, { ...asked.get(file), ...request });
1117
+ // Asked for, it is built with its stack - the one the call names, its own, or the one chosen now - pinned, whatever
1118
+ // is chosen later; a spec change keeps the one it was asked for with.
1119
+ const pin = { keep: specChange === true };
1120
+ const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request, pin)))));
1121
+ // Asked again, a halted codebase is tried again - one the person halted too, unless only its spec changed.
1122
+ for (const state of states) {
1123
+ state.halted = undefined;
1124
+ if (specChange !== true)
1125
+ state.held = undefined;
1126
+ }
1127
+ await tick();
1128
+ return [...new Set(states)].map(statusOf);
1129
+ };
1130
+ // Asked for, it shows as being planned at once; a spec change is looked at quietly, as every change is.
1131
+ return given.specChange === true ? now() : preparingFor(plainFile, now);
1092
1132
  },
1093
1133
  async regenerateAgain(plainFile, given, request = {}) {
1094
- const frids = [...new Set(Array.isArray(given) ? given : [given])];
1095
- const asked = resolve(options.cwd, plainFile);
1096
- const own = resolveRequiresChain(asked, [], undefined, { allowNoFunctionalities: true });
1097
- const module = tipOf(own).moduleName;
1098
- const chains = chainsFor(asked);
1099
- for (const chain of chains) {
1100
- const tip = tipOf(chain);
1101
- if (modules.get(tip.buildDir)?.running !== undefined)
1102
- throw new Error(`${tip.moduleName} has a pulse running: ask again once it is done.`);
1103
- }
1104
- const found = new Set();
1105
- for (const chain of chains) {
1106
- const tip = tipOf(chain);
1107
- const record = readRecord(tip.buildDir);
1108
- if (record === null)
1109
- continue;
1110
- const section = tip.moduleName === module ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS;
1111
- const items = record[section] ?? [];
1112
- const named = (item, index) => section === FUNCTIONAL_REQUIREMENTS ? String(index + 1) : item.module === module ? (item.frid ?? "") : "";
1113
- const taken = items.flatMap((item, index) => (frids.includes(named(item, index)) ? [named(item, index)] : []));
1114
- if (taken.length === 0)
1115
- continue;
1116
- for (const frid of taken)
1117
- found.add(frid);
1118
- const marked = items.map((item, index) => (frids.includes(named(item, index)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1119
- writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
1120
- // Its conformance tests were written against the code being replaced: marked with it, they are written again -
1121
- // checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
1122
- if (section === FUNCTIONAL_REQUIREMENTS) {
1123
- const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
1124
- const tests = readRecord(testsDir);
1125
- const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
1126
- if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
1127
- const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1128
- writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
1134
+ return preparingFor(plainFile, async () => {
1135
+ const frids = [...new Set(Array.isArray(given) ? given : [given])];
1136
+ const asked = resolve(options.cwd, plainFile);
1137
+ const own = resolveRequiresChain(asked, [], undefined, { allowNoFunctionalities: true });
1138
+ const module = tipOf(own).moduleName;
1139
+ const chains = chainsFor(asked);
1140
+ for (const chain of chains) {
1141
+ const tip = tipOf(chain);
1142
+ if (modules.get(tip.buildDir)?.running !== undefined)
1143
+ throw new Error(`${tip.moduleName} has a pulse running: ask again once it is done.`);
1144
+ }
1145
+ const found = new Set();
1146
+ for (const chain of chains) {
1147
+ const tip = tipOf(chain);
1148
+ const record = readRecord(tip.buildDir);
1149
+ if (record === null)
1150
+ continue;
1151
+ const section = tip.moduleName === module ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS;
1152
+ const items = record[section] ?? [];
1153
+ const named = (item, index) => section === FUNCTIONAL_REQUIREMENTS ? String(index + 1) : item.module === module ? (item.frid ?? "") : "";
1154
+ const taken = items.flatMap((item, index) => (frids.includes(named(item, index)) ? [named(item, index)] : []));
1155
+ if (taken.length === 0)
1156
+ continue;
1157
+ for (const frid of taken)
1158
+ found.add(frid);
1159
+ const marked = items.map((item, index) => (frids.includes(named(item, index)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1160
+ writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
1161
+ // Its conformance tests were written against the code being replaced: marked with it, they are written again -
1162
+ // checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
1163
+ if (section === FUNCTIONAL_REQUIREMENTS) {
1164
+ const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
1165
+ const tests = readRecord(testsDir);
1166
+ const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
1167
+ if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
1168
+ const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1169
+ writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
1170
+ }
1129
1171
  }
1172
+ recordHistory(tip.buildDir, `${module} functional spec${taken.length === 1 ? "" : "s"} ${taken.join(", ")} to regenerate again`);
1130
1173
  }
1131
- recordHistory(tip.buildDir, `${module} functional spec${taken.length === 1 ? "" : "s"} ${taken.join(", ")} to regenerate again`);
1132
- }
1133
- const missing = frids.filter((frid) => !found.has(frid));
1134
- if (found.size === 0)
1135
- throw new Error(`${module} has no functional spec ${missing.join(", ")} built anywhere, so there is nothing to regenerate again: regenerate builds what is not built.`);
1136
- const statuses = await loop.bringUp(plainFile, request);
1137
- if (missing.length > 0)
1138
- reportActivity(` ${module}: functional spec ${missing.join(", ")} is not built anywhere - nothing to regenerate again there`);
1139
- return statuses;
1174
+ const missing = frids.filter((frid) => !found.has(frid));
1175
+ if (found.size === 0)
1176
+ throw new Error(`${module} has no functional spec ${missing.join(", ")} built anywhere, so there is nothing to regenerate again: regenerate builds what is not built.`);
1177
+ const statuses = await loop.bringUp(plainFile, request);
1178
+ if (missing.length > 0)
1179
+ reportActivity(` ${module}: functional spec ${missing.join(", ")} is not built anywhere - nothing to regenerate again there`);
1180
+ return statuses;
1181
+ });
1140
1182
  },
1141
1183
  drift(plainFile) {
1142
1184
  const file = resolve(options.cwd, plainFile);
@@ -1285,21 +1327,23 @@ export function createWorkLoop(options) {
1285
1327
  return loop.bringUp(plainFile, request);
1286
1328
  },
1287
1329
  async fromScratch(plainFile, { chain, ...request } = {}) {
1288
- const file = resolve(options.cwd, plainFile);
1289
- // Where its code lives: the tips its module is built in, or - none built yet - its own codebase. The whole chain:
1290
- // every codebase in those chains, bottom first - a tip taken back to nothing is otherwise started from a copy of
1291
- // the code below it.
1292
- const chains = chainsFor(file);
1293
- const codebases = [...new Map((chain === true ? chains.flat() : chains.map(tipOf)).map((entry) => [entry.buildDir, entry])).values()];
1294
- for (const entry of codebases) {
1295
- if (modules.get(entry.buildDir)?.running !== undefined)
1296
- throw new Error(`${entry.moduleName} has a pulse running: halt it, or ask again once it is done.`);
1297
- }
1298
- const why = chain === true ? "asked to be regenerated from scratch, with its whole chain" : "asked to be regenerated from scratch";
1299
- for (const entry of codebases)
1300
- if (started(entry))
1301
- startAgain(entry, why);
1302
- return loop.bringUp(plainFile, request);
1330
+ return preparingFor(plainFile, async () => {
1331
+ const file = resolve(options.cwd, plainFile);
1332
+ // Where its code lives: the tips its module is built in, or - none built yet - its own codebase. The whole chain:
1333
+ // every codebase in those chains, bottom first - a tip taken back to nothing is otherwise started from a copy of
1334
+ // the code below it.
1335
+ const chains = chainsFor(file);
1336
+ const codebases = [...new Map((chain === true ? chains.flat() : chains.map(tipOf)).map((entry) => [entry.buildDir, entry])).values()];
1337
+ for (const entry of codebases) {
1338
+ if (modules.get(entry.buildDir)?.running !== undefined)
1339
+ throw new Error(`${entry.moduleName} has a pulse running: halt it, or ask again once it is done.`);
1340
+ }
1341
+ const why = chain === true ? "asked to be regenerated from scratch, with its whole chain" : "asked to be regenerated from scratch";
1342
+ for (const entry of codebases)
1343
+ if (started(entry))
1344
+ startAgain(entry, why);
1345
+ return loop.bringUp(plainFile, request);
1346
+ });
1303
1347
  },
1304
1348
  halt(target, reason) {
1305
1349
  const named = target.trim();
@@ -1346,62 +1390,81 @@ export function createWorkLoop(options) {
1346
1390
  ].join("");
1347
1391
  },
1348
1392
  status() {
1349
- return order.map((key) => statusOf(modules.get(key)));
1393
+ return [...order.map((key) => statusOf(modules.get(key))), ...preparingAlone().map(({ since: _since, ...status }) => status)];
1350
1394
  },
1351
1395
  planned() {
1352
1396
  const now = Date.now();
1353
1397
  let sequence = Number.MAX_SAFE_INTEGER;
1354
1398
  // The last job - running or planned - of each module, by name: what a module waiting for it waits behind.
1355
1399
  const lastOf = new Map();
1356
- return order.flatMap((key) => {
1357
- const state = modules.get(key);
1358
- const tip = tipEntry(state);
1359
- const status = statusOf(state);
1360
- let before = status.running?.jobId;
1361
- if (before !== undefined)
1362
- lastOf.set(tip.moduleName, before);
1363
- const row = (id, summary, note, waitingOn, conformance = false) => ({
1364
- id,
1365
- kind: conformance ? "conformance" : "regenerate",
1366
- summary,
1367
- module: tip.moduleName,
1368
- note,
1369
- ...(state.model !== undefined ? { model: state.model } : {}),
1370
- status: "queued",
1371
- sequence: sequence--,
1372
- queuedAt: now,
1373
- startedAt: now,
1374
- waitingOn,
1375
- tail: "",
1376
- activity: [],
1377
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
1378
- agents: [],
1379
- transcript: [],
1380
- });
1381
- // Waiting for the module below it: not planned yet, but in the queue all the same - behind what it waits for.
1382
- if (state.running === undefined && state.waitsFor !== undefined) {
1383
- const behind = lastOf.get(state.waitsFor);
1384
- const id = `planned-${tip.moduleName}-waiting`;
1385
- lastOf.set(tip.moduleName, id);
1386
- return [
1387
- row(id, `${basename(tip.plainFile)} - waits for ${state.waitsFor}`, `Not planned yet: its work is planned into pulses once ${state.waitsFor} is built - the code it is built on.`, behind !== undefined ? [behind] : []),
1388
- ];
1389
- }
1390
- return status.planned.map((job, index) => {
1391
- const planned = row(`planned-${tip.moduleName}-${index + 1}`, `${basename(tip.plainFile)} ${job.track === "conformance" ? "conformance: " : ""}${job.title}`, job.waitsFor !== undefined
1392
- ? `Scheduled, not started: the conformance tests of ${job.title}, once ${waitsText(job.waitsFor)} built - the code record has to have them first.`
1393
- : job.afterCode === true
1394
- ? "Not planned yet: the module's conformance tests are planned into pulses once its code is built."
1395
- : job.planning === true
1396
- ? `Being planned into pulses now: ${job.title}. The first starts as soon as the plan is made.`
1397
- : `Planned, not started: ${casesText(job.cases)}. It is planned again whenever the spec changes or a pulse before it ends.`, before !== undefined ? [before] : [], job.track === "conformance");
1398
- if (job.planning === true)
1399
- planned.planning = { since: state.planningSince ?? now };
1400
- before = planned.id;
1401
- lastOf.set(tip.moduleName, planned.id);
1402
- return planned;
1403
- });
1404
- });
1400
+ // A module asked for that the loop holds nothing of yet: being planned, from the moment it was asked for.
1401
+ const early = preparingAlone().map(({ module: name, since }) => ({
1402
+ id: `planned-${name}-preparing`,
1403
+ kind: "regenerate",
1404
+ summary: `${name}.plain - being planned`,
1405
+ module: name,
1406
+ note: "Being planned into pulses now. The first starts as soon as the plan is made.",
1407
+ status: "queued",
1408
+ sequence: sequence--,
1409
+ queuedAt: since,
1410
+ startedAt: since,
1411
+ waitingOn: [],
1412
+ tail: "",
1413
+ activity: [],
1414
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
1415
+ agents: [],
1416
+ transcript: [],
1417
+ planning: { since },
1418
+ }));
1419
+ return [...early, ...order.flatMap((key) => {
1420
+ const state = modules.get(key);
1421
+ const tip = tipEntry(state);
1422
+ const status = statusOf(state);
1423
+ let before = status.running?.jobId;
1424
+ if (before !== undefined)
1425
+ lastOf.set(tip.moduleName, before);
1426
+ const row = (id, summary, note, waitingOn, conformance = false) => ({
1427
+ id,
1428
+ kind: conformance ? "conformance" : "regenerate",
1429
+ summary,
1430
+ module: tip.moduleName,
1431
+ note,
1432
+ ...(state.model !== undefined ? { model: state.model } : {}),
1433
+ status: "queued",
1434
+ sequence: sequence--,
1435
+ queuedAt: now,
1436
+ startedAt: now,
1437
+ waitingOn,
1438
+ tail: "",
1439
+ activity: [],
1440
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
1441
+ agents: [],
1442
+ transcript: [],
1443
+ });
1444
+ // Waiting for the module below it: not planned yet, but in the queue all the same - behind what it waits for.
1445
+ if (state.running === undefined && state.waitsFor !== undefined) {
1446
+ const behind = lastOf.get(state.waitsFor);
1447
+ const id = `planned-${tip.moduleName}-waiting`;
1448
+ lastOf.set(tip.moduleName, id);
1449
+ return [
1450
+ row(id, `${basename(tip.plainFile)} - waits for ${state.waitsFor}`, `Not planned yet: its work is planned into pulses once ${state.waitsFor} is built - the code it is built on.`, behind !== undefined ? [behind] : []),
1451
+ ];
1452
+ }
1453
+ return status.planned.map((job, index) => {
1454
+ const planned = row(`planned-${tip.moduleName}-${index + 1}`, `${basename(tip.plainFile)} ${job.track === "conformance" ? "conformance: " : ""}${job.title}`, job.waitsFor !== undefined
1455
+ ? `Scheduled, not started: the conformance tests of ${job.title}, once ${waitsText(job.waitsFor)} built - the code record has to have them first.`
1456
+ : job.afterCode === true
1457
+ ? "Not planned yet: the module's conformance tests are planned into pulses once its code is built."
1458
+ : job.planning === true
1459
+ ? `Being planned into pulses now: ${job.title}. The first starts as soon as the plan is made.`
1460
+ : `Planned, not started: ${casesText(job.cases)}. It is planned again whenever the spec changes or a pulse before it ends.`, before !== undefined ? [before] : [], job.track === "conformance");
1461
+ if (job.planning === true)
1462
+ planned.planning = { since: state.planningSince ?? preparing.get(tip.moduleName)?.since ?? now };
1463
+ before = planned.id;
1464
+ lastOf.set(tip.moduleName, planned.id);
1465
+ return planned;
1466
+ });
1467
+ })];
1405
1468
  },
1406
1469
  firsts() {
1407
1470
  return order.flatMap((key) => {
@@ -1467,7 +1530,7 @@ export function createWorkLoop(options) {
1467
1530
  return loop;
1468
1531
  }
1469
1532
  /** Why nothing runs in a codebase whose code - and conformance tests, where they are wanted - is built from its spec as it is. */
1470
- export const UP_TO_DATE = "built from the spec as it is";
1533
+ export const UP_TO_DATE = "built from the ***plain as it is";
1471
1534
  /** Why a codebase with work left has no plan: the planner found none. */
1472
1535
  const NO_PLAN = "the planner found no way to cut the work into pulses that hold together";
1473
1536
  /** A diff's lines placed in the .plain files of a chain - the tip's first - named as they are from the project. */
@@ -1570,13 +1633,13 @@ export function describeModules(statuses, options = {}) {
1570
1633
  .map((status) => {
1571
1634
  // Nothing to do there: said in a sentence, not as a status.
1572
1635
  if (isUpToDate(status))
1573
- return `${status.module} is up to date: its code is built from the spec as it is now in ${basename(status.plainFile)}, so there is nothing to regenerate.`;
1636
+ return `${status.module} is up to date: its code is built from the ***plain as it is now in ${basename(status.plainFile)}, so there is nothing to regenerate.`;
1574
1637
  const lines = [`${status.module} (${basename(status.plainFile)}${status.stack !== undefined ? `, stack ${status.stack}` : ""}):`];
1575
1638
  if (status.running !== undefined) {
1576
1639
  lines.push(` running: pulse ${status.running.jobId} - ${status.running.title}`);
1577
1640
  // Steered, not stopped or planned again: what changed went to its agent.
1578
1641
  for (const why of status.running.steered ?? [])
1579
- lines.push(` ⇢ steered: ${why} - its agent was told, and it lands the spec as it is now there`);
1642
+ lines.push(` ⇢ steered: ${why} - its agent was told, and it lands the ***plain as it is now there`);
1580
1643
  }
1581
1644
  status.planned.forEach((job, index) => lines.push(job.afterCode === true
1582
1645
  ? ` ${index + 1}. conformance tests - planned once the code is built`
@@ -332,7 +332,7 @@ Either way the choice is made inside the pulse, so it is made - and its conforma
332
332
 
333
333
  **Scheduled ahead of the code.** `conform` may name functional specs whose code is not built yet: each call schedules them as one group, shown at once in what is planned, after the module's last code job. Its functional specs are chosen once the code record has every one as \*\*\*plain has it, and its tests are written once the module's code is built. A functional spec \*\*\*plain does not have - built by no code job - or a module with no test reqs is refused, and nothing is scheduled; one taken out of \*\*\*plain later is dropped from its group. Groups whose code is built by then are tested together, in one job with the rest, as far as the guardrail lets.
334
334
 
335
- While a codebase's work is being planned into jobs, with nothing running there, what is planned says so - the conformance tests of the groups being planned, or the work - rather than showing nothing until the plan is made.
335
+ While a codebase's work is being planned into jobs, with nothing running there, what is planned says so - the conformance tests of the groups being planned, or the work - rather than showing nothing until the plan is made. A module asked for - `regenerate`, from scratch, functionalities again - shows its work as being planned from that moment, before its code is deleted and what is left is worked out, not only once the planner runs: in the loop's status, which a host draws its plan from, and in the job panel - a module the loop holds nothing of yet, as when turri was just opened, too. A \*\*\*plain change is looked at quietly.
336
336
 
337
337
  **What a conformance job tests is built.** As a conformance job starts, the harness reads the code record, `code/.turri.json`, and checks that it has every functional spec the job adds or changes tests for, as the job has it. One it does not have stops the job before any agent runs: its report says which are not built, the conformance tests halt, and the chat model tells the person. They are tried again once the code or \*\*\*plain changes. Code jobs and conformance jobs are planned apart, each track on its own: a job is one or the other. A module built on another starts with an empty tests record: the tests it inherits are the module below's, and none of its own is written yet.
338
338
 
package/docs/using.md CHANGED
@@ -59,7 +59,7 @@ A pulse is one unit of work regenerating the code. It was called a job, and stil
59
59
 
60
60
  A regeneration takes minutes, so in the TUI every tool that regenerates starts the work as a pulse and returns straight away. The conversation carries on, and you need not wait for a pulse to finish: go on shaping the \*\*\*plain and writing its next change, ask questions, try what has landed - turri says so as each pulse starts, and never waits for one itself. A \*\*\*plain change made meanwhile steers the running pulse or is planned again around it (see below); pulses on the same module - or on modules of one `requires` chain - queue behind each other, in the order they were asked for, from the first pulse on: one queued while the module's \*\*\*plain is only a scaffold, with no functional specs yet, or while it does not parse, still holds its module, so nothing on the module runs beside it. Pulses on modules that share nothing run side by side, each with its own progress panel - two modules or ten. How many run at once is capped by what the machine has room for: past `parallel-jobs` (see [the project config](config.md#how-many-jobs-at-once)), a pulse whose turn has come waits for a free slot, and says so.
61
61
 
62
- - **The panel** above the input, while something runs, shows on top what finished - a line a module, the module that finished last on top, its pulses newest first, each with its mark (green, red or amber, as in the side panel), number and kind - what it regenerated is in the side panel - `cognio ✓ 4 conformance · ✓ 3 regenerate · … 2 earlier`, the newest that fit the width - then a rule, and under it each running pulse, the newest first - `cognio · regenerate pulse 5`, `cognio · conformance pulse 6` - its time and cost, and what its agents are doing. A module whose work is being planned into pulses shows at once, before any pulse runs - `shorty · planning the pulses · 12s` - the planner is an agent, and takes a while; a `/regenerate` says it began the moment it is sent. Pulses are numbered within their module, as regen numbers them. `ctrl+o` expands the agents' trace alone; what finished stays a line a module. The line of keys under it - `ctrl+o to expand the trace · ctrl+h to peek at regen` - has each key breathing, fully there to a little faint and back, until it is first pressed: once `ctrl+o` has opened the trace, or the side panel has been opened, that key is still for the rest of the session and the other breathes on. What waits is not in it, and it shows no \*\*\*plain diff - the side panel has both. With nothing running it is gone.
62
+ - **The panel** above the input, while something runs, shows on top what finished - a line a module, the module that finished last on top, its pulses newest first, each with its mark (green, red or amber, as in the side panel), number and kind - what it regenerated is in the side panel - `cognio ✓ 4 conformance · ✓ 3 regenerate · … 2 earlier`, the newest that fit the width - then a rule, and under it each running pulse, the newest first - `cognio · regenerate pulse 5`, `cognio · conformance pulse 6` - its time and cost, and what its agents are doing. A module whose work is being planned into pulses shows at once, before any pulse runs - `shorty · planning the pulses · 12s` - from the moment it is asked for: while its code is deleted for a start from scratch and what is left to regenerate is worked out, as well as while the planner, an agent, cuts it into pulses; a `/regenerate` says it began the moment it is sent. A \*\*\*plain change is looked at quietly, and shows only once the planner runs. Pulses are numbered within their module, as regen numbers them. `ctrl+o` expands the agents' trace alone; what finished stays a line a module. The line of keys under it - `ctrl+o to expand the trace · ctrl+h to peek at regen` - has each key breathing, fully there to a little faint and back, until it is first pressed: once `ctrl+o` has opened the trace, or the side panel has been opened, that key is still for the rest of the session and the other breathes on. What waits is not in it, and it shows no \*\*\*plain diff - the side panel has both. With nothing running it is gone.
63
63
  - **A pulse's change** is what its agent works from, not a text diff of the module's file: the items `<spec change>` gives the agent - only what the pulse's functional specs use, an import's items as well as the module's own, a code pulse's without the test reqs - each in the file it is written in, under its section, in the file's order (`change-view.ts`).
64
64
  - **A \*\*\*plain diff**, in the side panel, reads as an editor shows a diff: its [\*\*\*plain](https://plainlang.org/docs/) highlighted as [plyn](https://github.com/plainlang/plyn) highlights it - section headers, concepts, names in backticks, text in quotes, links, the front matter - each added line filled green and each removed one red, the text on top, the fill the width of the panel ([colours](theming.md#plain-diffs)). It is shown file by file, each file's lines together under its name, with what it adds and takes out - the module being regenerated first, the rest by name, and none the diff does not change. Each line has its number in that file: a \*\*\*plain diff is of \*\*\*plain as read, with what it imports laid in, so a line is placed in the file it is written in - its links read as the parser renders them - and a removed line, in no file now, goes with the change it is part of and has no number. A line is never put in a file on a guess: one that cannot be placed is listed last, under "not found in the spec's files". Where lines of a file are not in the diff, `↕ 28 lines` says how many, as GitHub does - `↑` above the first - and names the section the lines under it are in when it is another than the one above, `↕ 28 lines · ***functional specs***`, as GitHub names a hunk's function: each section is named once.
65
65
  - **The side panel** (`ctrl+h`, as it was left - but on the pulse that last failed, stopped or did not pass, when one has since it was last opened: the live panel that showed it is gone with the last pulse, and its announcement in the chat says `ctrl+h shows this pulse`; **`/pulses`** on the pulses; not `ctrl+j`, which a terminal without the Kitty keyboard protocol sends as a line feed, the same as `shift+enter`, and which a dialog takes for `enter`; both stay [pi](https://pi.dev)'s new-line keys. `ctrl+h` is the byte old terminals sent for backspace; a terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time - or, `s`, sits beside the chat: the chat on the left, the panel in the right half of the screen, and `s` again over the whole screen. It opens as it was last left, for the conversation; a terminal under 104 columns - 100 inside the screen's padding, a blank row above and below everything and two columns either side - has no room for both and shows the panel over the whole screen. It opens on **regen**: the module's \*\*\*plain itself, in full, with how much of it the code has been regenerated from drawn onto it.
@@ -92,7 +92,7 @@ A regeneration takes minutes, so in the TUI every tool that regenerates starts t
92
92
  - **`/stack`** chooses the stack everything is built with from then on, in a project whose parts are on different stacks: a list of the project's stacks, or `/stack backend`, `/stack none`. The stack's `config.turri.<stack>.yaml` is laid over `config.turri.yaml` - its testing scripts, toolchains, models - for the session; a module already asked for keeps the stack it was asked for with. The chat model switches it too when you ask - and, to build one part, passes that part's stack to `regenerate` instead, leaving the session's as it is. See [stacks](config.md#stacks).
93
93
  - **`/regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]`** regenerates a module from its \*\*\*plain as it is now, as the `regenerate` tool does: what is left to regenerate is planned into pulses - one functional spec at a time, a few together or the whole module in one, as the planner judges - and the first starts. Words after the module are guidance for that plan (`/regenerate app models first`). A module's name typed with spaces for its underscores is that module - `/regenerate d365_function executor` is `d365_function_executor`. With no module named: the one `module:` in `config.turri.yaml` names, else the folder's top modules, to pick from. Asked for from scratch - `from scratch`, `start over`, `delete the module` - it asks first, then deletes the module's code and conformance tests in every codebase where they live, committed so `/lineage` keeps them, and regenerates the whole module from its \*\*\*plain, built though nothing in it changed: `/regenerate shorty from scratch`. The modules below it keep their code, and the module starts again from a copy of the code below it. Asked for its chain too - `/regenerate shorty from scratch, the whole chain` - the code of every module of its requires chain is deleted, the base's included, and the whole chain is regenerated from the bottom: what to do after modules were split, merged or moved, when the code below was laid out for modules that are no more. The chat's `regenerate` does the same with `from_scratch: "module"` or `"chain"`, when you ask for it; `turri regenerate --from-scratch` or `--from-scratch-chain` on the command line, not with `--dry-run`. Functionalities named after the module - `/regenerate shorty 3 5` - are regenerated though their \*\*\*plain did not change, on the model chosen for functionalities: as `turri regenerate shorty.plain 3 5` does, and the chat's `regenerate` with `functionalities`.
94
94
  - **`/halt <id>`** halts a pulse - a queued one never starts - **`/halt <module>`** (or a planned row's id) halts a module's planned pulses until you ask for it again, a \*\*\*plain change not starting it, and **`/halt activated <name>`** halts a server `activate` started; it lists all three when no id is given.
95
- - **What these commands show you, the chat knows.** What `/regenerate` and `/halt activated` tell you - \*\*\*plain that does not parse, the pulses planned, a server halted - stays in the chat under the command, and since a slash command runs outside the conversation, it is put in the conversation as well: say "fix it" next and turri knows what you are looking at. A halted pulse reaches it as every pulse's end does.
95
+ - **What these commands show you, the chat knows.** What `/regenerate` and `/halt activated` tell you - \*\*\*plain that does not parse, the pulses planned, a server halted - stays in the chat under the command, and since a slash command runs outside the conversation, it is put in the conversation as well: say "fix it" next and turri knows what you are looking at. Once `/regenerate` has started pulses, turri says so in the chat too - what runs and what is planned, and that you need not wait for it - without starting anything more for it; a module already up to date gets the plan's answer alone. A halted pulse reaches it as every pulse's end does.
96
96
  - **Nothing is queued.** What is left to build is worked out from what each codebase is built from and \*\*\*plain as it is now - again whenever a pulse ends or \*\*\*plain changes - and the first pulse of it runs; see [a whole module](regenerating.md#a-whole-module). Changes that overlap never pile up: the next pulse builds \*\*\*plain as it is. The pulses planned after the one running are listed behind it, as queued, and so is each module waiting for the one it is built on - "waits for base" - until it can be planned. A change inside what the running pulse regenerates - a functional spec it regenerates reworded or given a sub-bullet, a definition or req it is given, a file they link - steers it: its agent is told, and it lands \*\*\*plain as it is now there, twice a pulse at most. Otherwise a pulse building something \*\*\*plain no longer wants is stopped and rolled back - unless `allow-to-stop-jobs: false` in `config.turri.yaml`, which has it land first and the change regenerated on top. To drop something, take it out of \*\*\*plain - one \*\*\*plain diff. Stopping a pulse leaves its change in \*\*\*plain: the codebase waits until its \*\*\*plain changes or it is asked for again.
97
97
  - **A change to a module below** is made in the codebase at the tip of the chain, where its code lives; the codebase below is left as it was, and is brought up to its own \*\*\*plain only when a new module is to start from it. \*\*\*plain moved into a module the code was not built on stops the codebase: no change to its code brings that module in, so it has to be started over - its code and conformance tests taken out, kept in its history, and built again from the bottom of the chain. That is never done unasked: the chat asks you, saying why, and regenerates it from scratch once you agree; on the command line `turri regenerate` asks at the terminal, and `--start-over` agrees in advance.
98
98
  - **Conformance tests are a track of their own.** A code pulse lands once its unit tests pass and are reviewed. Then, before the next one builds on it, a conformance pulse writes and runs the tests of what it built - from the test reqs and acceptance tests - with the whole suite the module inherits, and, where one fails, fixes the test or the code, whichever is wrong; what they are written for is recorded in `tests/.turri.json`. With `conformance-tests: false` none is written until you ask - `conform`, for a module or some of its functional specs. A conformance pulse that does not pass is undone whole and halts the conformance tests alone: the code goes on being built.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.24-next.2b2f203",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {