@turri-dev/cli 0.1.3 → 0.1.4-next.318a98a

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 CHANGED
@@ -27,7 +27,7 @@ npm install -g @turri-dev/cli # puts `turri` on your PATH
27
27
  turri doctor # checks the machine
28
28
  ```
29
29
 
30
- To work on turri itself, run it from a clone instead - see [installation](docs/installation.md#from-a-clone).
30
+ To follow `main` as it moves, or work on turri itself, run it [from a clone](#from-a-clone) instead.
31
31
 
32
32
  `doctor` checks for Node 22.19+, bash, the OS sandbox and a model provider for pi; in a project, the toolchains its
33
33
  modules need, what its testing scripts run against this machine, and that each model `config.turri.yaml` names
@@ -73,6 +73,27 @@ Rather not install Node and the rest? **turri** also runs in Docker, from one im
73
73
 
74
74
  Choosing models: [models](#models); what to do next: [use](#use).
75
75
 
76
+ ### From a clone
77
+
78
+ In place of the npm package - for the latest of `main`, or to change turri itself:
79
+
80
+ ```bash
81
+ git clone git@github.com:turri-oss/turri.git && cd turri
82
+ npm install
83
+ npm link # puts `turri` on your PATH, pointing at this clone
84
+ turri doctor
85
+ ```
86
+
87
+ The clone runs its TypeScript sources directly, so there is nothing to build. When new commits land:
88
+
89
+ ```bash
90
+ git pull
91
+ npm install # picks up any dependency that changed
92
+ ```
93
+
94
+ A **turri** already running keeps the code it started with - start it again to run what you pulled. More in
95
+ [installation](docs/installation.md#from-a-clone).
96
+
76
97
  ## Why
77
98
 
78
99
  In the age of coding agents, writing code is getting easy. The hard and important part is
package/dist/extension.js CHANGED
@@ -548,7 +548,7 @@ export default function (pi) {
548
548
  loopTools: [
549
549
  regenerateTool({ cwd, jobs, loop: () => workLoopFor(jobs, cwd, ctx), model: (_skill, plainFile, named, work) => jobModel(ctx, work ?? "spec_changes", named, plainFile), wait: () => false }),
550
550
  driftTool({ cwd, jobs, loop: () => workLoopFor(jobs, cwd, ctx) }),
551
- haltTool({ jobs }),
551
+ haltTool({ jobs, workLoop: () => workLoops.get(jobs)?.loop }),
552
552
  conformTool({ cwd, loop: () => workLoopFor(jobs, cwd, ctx), model: (_skill, plainFile, named) => jobModel(ctx, "spec_changes", named, plainFile), wait: () => false }),
553
553
  ],
554
554
  // The escalation becomes a real dialog in the TUI. Without dialog-capable
@@ -1419,25 +1419,37 @@ function registerJobTools(pi, jobs, cwd) {
1419
1419
  // The panel is compact and ctrl+o only lasts as long as the work does. This is the whole
1420
1420
  // list, finished jobs included, put in the transcript where it stays.
1421
1421
  pi.registerCommand("halt", {
1422
- description: "Halt a queued or running pulse, or what activate started: /halt [pulse id | activated <name>]",
1422
+ description: "Halt a queued or running pulse, a module's planned pulses, or what activate started: /halt [pulse id | module | activated <name>]",
1423
1423
  handler: async (args, ctx) => {
1424
1424
  if (!ctx.hasUI)
1425
1425
  return;
1426
1426
  const live = jobs.list().filter((job) => job.status === "running" || job.status === "queued");
1427
1427
  const deployed = runningDeployed();
1428
+ const loop = workLoops.get(jobs)?.loop;
1429
+ // A module's planned pulses, one line a module: halting one halts the module.
1430
+ const plannedRows = loop?.planned() ?? [];
1431
+ const planned = [...new Set(plannedRows.map((row) => row.module ?? ""))].filter((module) => module !== "");
1428
1432
  let id = args.trim();
1429
1433
  if (id === "") {
1430
- if (live.length === 0 && deployed.length === 0)
1431
- return ctx.ui.notify("Nothing is regenerating or queued.", "info");
1434
+ if (live.length === 0 && deployed.length === 0 && planned.length === 0)
1435
+ return ctx.ui.notify("Nothing is regenerating, queued or planned.", "info");
1432
1436
  const label = (job) => `${job.id} ${job.status.padEnd(8)} ${pulseName(job)}`;
1433
- const labels = [...live.map(label), ...deployed.map((item) => deployedLabel(item.name, item.url ?? item.command))];
1437
+ const plannedLabel = (module) => {
1438
+ const rows = plannedRows.filter((row) => row.module === module);
1439
+ const waiting = rows.find((row) => row.id.endsWith("-waiting"));
1440
+ return `${module} ${waiting !== undefined ? waiting.summary : `${rows.length} planned`} - halts the module`;
1441
+ };
1442
+ const labels = [...live.map(label), ...planned.map(plannedLabel), ...deployed.map((item) => deployedLabel(item.name, item.url ?? item.command))];
1434
1443
  const picked = await ctx.ui.select("Halt which?", labels);
1435
1444
  const job = live.find((candidate) => picked !== undefined && label(candidate) === picked);
1445
+ const module = planned.find((name) => picked !== undefined && plannedLabel(name) === picked);
1436
1446
  const server = deployed.find((item) => picked !== undefined && deployedLabel(item.name, item.url ?? item.command) === picked);
1437
1447
  if (server !== undefined)
1438
1448
  id = `activated ${server.name}`;
1439
1449
  else if (job !== undefined)
1440
1450
  id = job.id;
1451
+ else if (module !== undefined)
1452
+ id = module;
1441
1453
  else
1442
1454
  return;
1443
1455
  }
@@ -1446,7 +1458,7 @@ function registerJobTools(pi, jobs, cwd) {
1446
1458
  const stopped = await stopDeployed({ name });
1447
1459
  return shownAfterCommand(pi, ctx, `/halt ${id}`, stopped.length > 0 ? `Halted ${stopped.join(", ")}.` : `Nothing activate started is running as ${name}.`, "info");
1448
1460
  }
1449
- ctx.ui.notify(stopJob(jobs, id), "info");
1461
+ ctx.ui.notify(stopJob(jobs, id, undefined, loop), "info");
1450
1462
  },
1451
1463
  });
1452
1464
  // Debug mode, for the render agents started from now on - see debug-log.ts.
@@ -2037,6 +2049,7 @@ function registerDeploy(pi, cwd, jobs, state) {
2037
2049
  cwd,
2038
2050
  state,
2039
2051
  jobs,
2052
+ workLoop: () => workLoops.get(jobs)?.loop,
2040
2053
  defaultPlainFile: () => readProjectConfig(cwd).module,
2041
2054
  });
2042
2055
  for (const tool of tools)
package/dist/genome.js CHANGED
@@ -185,7 +185,7 @@ export async function runSkill(opts) {
185
185
  // Built renders were lost with the process; queue what the specs hold that is not built.
186
186
  if (resumed !== undefined && opts.renderQueue !== undefined) {
187
187
  for (const file of plainFilesIn(opts.cwd))
188
- void opts.renderQueue.bringUp(file).catch(() => undefined);
188
+ void opts.renderQueue.bringUp(file, { specChange: true }).catch(() => undefined);
189
189
  }
190
190
  if (liveStep !== undefined && liveProxy !== undefined) {
191
191
  const continuous = liveStep.gate.continuous;
@@ -70,7 +70,11 @@ export async function loopReply(loop, statuses, wait) {
70
70
  return { content: [{ type: "text", text }], details: { jobIds } };
71
71
  }
72
72
  /** Halt a pulse by its id, or the start of it: what it was, and what moves up behind it. `reason`: why, kept with it. */
73
- export function stopJob(jobs, named, reason) {
73
+ export function stopJob(jobs, named, reason, loop) {
74
+ // A planned row, or a module by its name: the module is halted - its rows are planned again from the spec each look.
75
+ const module = /^planned-/.test(named.trim()) || !jobs.list().some((job) => job.id.startsWith(named.trim())) ? loop?.halt(named, reason) : undefined;
76
+ if (module !== undefined)
77
+ return module;
74
78
  const matches = jobs.list().filter((job) => job.id.startsWith(named.trim()));
75
79
  if (matches.length === 0)
76
80
  return `No pulse ${named}. /pulses shows them.`;
@@ -188,7 +192,7 @@ export function driftTool(options) {
188
192
  }
189
193
  const ACTIVATED_PARAM = Type.Optional(Type.String({ description: 'What activate started, by name - or "all" for every one' }));
190
194
  const HALT_PULSE_PARAMS = Type.Object({
191
- job_id: Type.Optional(Type.String({ description: "The pulse's id, or the start of it" })),
195
+ job_id: Type.Optional(Type.String({ description: "The pulse's id, or the start of it; a planned row's id (planned-<module>-...), or a module's name, halts the module" })),
192
196
  reason: Type.Optional(Type.String({ description: "Why it is halted, kept with it - what the spec now says that it did not, say" })),
193
197
  activated: ACTIVATED_PARAM,
194
198
  });
@@ -201,12 +205,21 @@ export function haltTool(options) {
201
205
  name: "halt",
202
206
  label: "Halt",
203
207
  description: jobs !== undefined
204
- ? "Halt a pulse, or what activate started. A pulse: a queued one never starts, a running one has its agent stopped at once, " +
205
- "and the pulses queued behind it move up - what a running pulse already changed is rolled back, and what it was regenerating " +
206
- "counts as not regenerated. What activate started: the one named, or every one."
208
+ ? "Halt a pulse, a module's planned pulses, or what activate started. A pulse: a queued one never starts, a running one has its " +
209
+ "agent stopped at once, and the pulses queued behind it move up - what a running pulse already changed is rolled back, and what " +
210
+ "it was regenerating counts as not regenerated. A planned row (planned-<module>-...) or a module's name halts the module: its " +
211
+ "running pulse stopped, none of its planned pulses started, and the modules built on it waiting - until it is asked for again " +
212
+ "with regenerate; a spec change does not start it. A module whose .plain file is gone is dropped. What activate started: the " +
213
+ "one named, or every one."
207
214
  : "Stop what activate started: the one named, or every one.",
208
215
  promptSnippet: jobs !== undefined ? "halt a pulse, or what activate started" : "stop a deployed server, or all of them",
209
- promptGuidelines: jobs !== undefined ? ["Halt a pulse only when the user asks, or your instructions say to. Name which one, and say which pulses were queued behind it - they will now run."] : [],
216
+ promptGuidelines: jobs !== undefined
217
+ ? [
218
+ "Halt a pulse only when the user asks, or your instructions say to. Name which one, and say which pulses were queued behind it - they will now run.",
219
+ "Halting a planned row halts its whole module - one planned pulse cannot be taken out alone, as the plan is made again from the spec. To build some of it later, or cut it differently, use steer; to drop work, change the spec.",
220
+ "A planned row for a module that is no longer in the project is halted the same way, and dropped.",
221
+ ]
222
+ : [],
210
223
  parameters: (jobs !== undefined ? HALT_PULSE_PARAMS : HALT_ACTIVATED_PARAMS),
211
224
  async execute(_id, params) {
212
225
  if (params.job_id === undefined && params.activated === undefined) {
@@ -214,7 +227,7 @@ export function haltTool(options) {
214
227
  }
215
228
  const lines = [];
216
229
  if (params.job_id !== undefined && jobs !== undefined)
217
- lines.push(stopJob(jobs, params.job_id, params.reason));
230
+ lines.push(stopJob(jobs, params.job_id, params.reason, options.workLoop?.()));
218
231
  if (params.activated !== undefined) {
219
232
  const stopped = await stopDeployed(params.activated === "all" ? {} : { name: params.activated });
220
233
  lines.push(stopped.length > 0 ? `Halted ${stopped.join(", ")}.` : `Nothing activate started is running${params.activated === "all" ? "" : ` as ${params.activated}`}.`);
@@ -378,6 +378,8 @@ export function createWorkLoop(options) {
378
378
  const waiting = (why) => ({ module: entry.moduleName, why: `waiting for ${entry.moduleName}: ${why}` });
379
379
  if (below.running !== undefined)
380
380
  return waiting(below.running.planned.track === "conformance" ? "its conformance tests are being written" : "the code it is built on is not built yet");
381
+ if (below.held !== undefined)
382
+ return waiting("you halted it - nothing is built on it until it is asked for again");
381
383
  const spec = chainSpec(below.chain);
382
384
  if (codeWork(builtOf(below), spec.tree).length > 0)
383
385
  return waiting("the code it is built on is not built yet");
@@ -486,6 +488,11 @@ export function createWorkLoop(options) {
486
488
  /** A dry run's first jobs, by codebase: what each would start. */
487
489
  const firsts = new Map();
488
490
  const startJob = (state, spec, from, planned) => {
491
+ // Halted while it was being planned: the plan is made, nothing starts.
492
+ if (state.held !== undefined) {
493
+ state.why = state.held.why;
494
+ return;
495
+ }
489
496
  const tip = tipEntry(state);
490
497
  if (options.dryRun === true) {
491
498
  firsts.set(tip.buildDir, { spec: planned.track === "conformance" ? spec : { ...spec, tree: planned.step.tree }, planned });
@@ -773,16 +780,37 @@ export function createWorkLoop(options) {
773
780
  }
774
781
  return states;
775
782
  };
783
+ /** A codebase the loop no longer looks after: nothing of it is shown, planned or waited for. */
784
+ const forget = (state) => {
785
+ const key = tipEntry(state).buildDir;
786
+ modules.delete(key);
787
+ order.splice(order.indexOf(key), 1);
788
+ };
776
789
  const lookAll = async () => {
790
+ // A .plain file taken out of the project is asked for no more, and a codebase whose module's file is gone is
791
+ // forgotten - nothing running there - rather than shown waiting for a plan it will never have.
792
+ for (const file of [...asked.keys()])
793
+ if (!existsSync(file))
794
+ asked.delete(file);
795
+ for (const key of [...order]) {
796
+ const state = modules.get(key);
797
+ if (state.running === undefined && !existsSync(tipEntry(state).plainFile))
798
+ forget(state);
799
+ }
777
800
  const now = active();
778
801
  looking = now;
779
802
  for (const key of order) {
780
803
  const state = modules.get(key);
781
- // A codebase left behind - built on since - is not looked at, unless a job of it is still running.
804
+ // A codebase left behind - built on since - is not looked at, unless a job of it is still running; what it had
805
+ // planned, or waited for, is no longer shown.
782
806
  if (now.has(state) || state.running !== undefined)
783
807
  await look(state);
784
- else
808
+ else {
785
809
  state.why = "left behind: the code is built on further up the chain, where changes to it are made";
810
+ state.waitsFor = undefined;
811
+ state.plan = undefined;
812
+ state.conformancePlan = undefined;
813
+ }
786
814
  }
787
815
  };
788
816
  const look = async (state) => {
@@ -793,6 +821,12 @@ export function createWorkLoop(options) {
793
821
  }
794
822
  catch (error) {
795
823
  state.why = `its spec does not parse: ${error instanceof Error ? error.message : String(error)}`;
824
+ // Not planned from a spec that does not parse: what it waited for, and the plan, were the spec's before.
825
+ state.waitsFor = undefined;
826
+ if (state.running === undefined) {
827
+ state.plan = undefined;
828
+ state.conformancePlan = undefined;
829
+ }
796
830
  return;
797
831
  }
798
832
  const specKey = keyOf(spec);
@@ -845,6 +879,11 @@ export function createWorkLoop(options) {
845
879
  return;
846
880
  }
847
881
  hashResources(state, spec);
882
+ if (state.held !== undefined) {
883
+ state.why = state.held.why;
884
+ state.waitsFor = undefined;
885
+ return;
886
+ }
848
887
  if (state.halted !== undefined) {
849
888
  if (state.halted.specKey === specKey) {
850
889
  state.why = state.halted.why;
@@ -1005,14 +1044,18 @@ export function createWorkLoop(options) {
1005
1044
  return state;
1006
1045
  };
1007
1046
  const loop = {
1008
- async bringUp(plainFile, request = {}) {
1047
+ async bringUp(plainFile, given = {}) {
1048
+ const { specChange, ...request } = given;
1009
1049
  const files = codebasesOf(resolve(options.cwd, plainFile));
1010
1050
  for (const file of files)
1011
1051
  asked.set(file, { ...asked.get(file), ...request });
1012
1052
  const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request)))));
1013
- // Asked again, a halted codebase is tried again.
1014
- for (const state of states)
1053
+ // Asked again, a halted codebase is tried again - one the person halted too, unless only its spec changed.
1054
+ for (const state of states) {
1015
1055
  state.halted = undefined;
1056
+ if (specChange !== true)
1057
+ state.held = undefined;
1058
+ }
1016
1059
  await tick();
1017
1060
  return [...new Set(states)].map(statusOf);
1018
1061
  },
@@ -1080,7 +1123,7 @@ export function createWorkLoop(options) {
1080
1123
  report.scheduled = (state.scheduled ?? []).map((group) => ({ frids: group.specs.map((touched) => touched.frid), unbuilt: group.unbuilt }));
1081
1124
  if (state.running !== undefined)
1082
1125
  report.running = { jobId: state.running.jobId, title: state.running.planned.title };
1083
- const halted = state.halted?.why ?? state.conformanceHalted?.why;
1126
+ const halted = state.held?.why ?? state.halted?.why ?? state.conformanceHalted?.why;
1084
1127
  if (halted !== undefined)
1085
1128
  report.why = halted;
1086
1129
  if (state.startOver !== undefined)
@@ -1167,6 +1210,7 @@ export function createWorkLoop(options) {
1167
1210
  }
1168
1211
  state.conformanceHalted = undefined;
1169
1212
  state.halted = undefined;
1213
+ state.held = undefined;
1170
1214
  }
1171
1215
  await tick();
1172
1216
  return [...new Set(states)].map((state) => {
@@ -1209,6 +1253,50 @@ export function createWorkLoop(options) {
1209
1253
  startAgain(entry, "asked to be regenerated from scratch");
1210
1254
  return loop.bringUp(plainFile, request);
1211
1255
  },
1256
+ halt(target, reason) {
1257
+ const named = target.trim();
1258
+ // A planned row's module - the longest name its id starts with, as a name may hold a hyphen - or a module by its name.
1259
+ const state = order
1260
+ .map((key) => modules.get(key))
1261
+ .filter((candidate) => {
1262
+ const name = tipEntry(candidate).moduleName;
1263
+ return named === name || named.startsWith(`planned-${name}-`);
1264
+ })
1265
+ .sort((a, b) => tipEntry(b).moduleName.length - tipEntry(a).moduleName.length)[0];
1266
+ if (state === undefined)
1267
+ return undefined;
1268
+ const tip = tipEntry(state);
1269
+ if (state.running === undefined && !existsSync(tip.plainFile)) {
1270
+ forget(state);
1271
+ asked.delete(tip.plainFile);
1272
+ return `${tip.moduleName} is no longer in the project - ${basename(tip.plainFile)} is gone - so nothing was to be built there: it is dropped from the regeneration.`;
1273
+ }
1274
+ const planned = statusOf(state).planned.length;
1275
+ const said = reason !== undefined && reason.trim() !== "" ? ` (${reason.trim()})` : "";
1276
+ state.held = { why: `you halted it${said} - nothing runs here until it is asked for again` };
1277
+ state.why = state.held.why;
1278
+ state.waitsFor = undefined;
1279
+ state.plan = undefined;
1280
+ state.conformancePlan = undefined;
1281
+ const running = state.running;
1282
+ if (running !== undefined && running.stopping !== true) {
1283
+ running.stopping = true;
1284
+ options.registry.stop(running.jobId, `Halted by you${said}: what it had done is rolled back, and nothing more runs in ${tip.moduleName} until it is asked for again.`);
1285
+ }
1286
+ const above = order
1287
+ .map((key) => modules.get(key))
1288
+ .filter((other) => other !== state && looking.has(other) && other.chain.some((entry) => entry.buildDir === tip.buildDir))
1289
+ .map((other) => tipEntry(other).moduleName);
1290
+ void tick();
1291
+ return [
1292
+ `Halted ${tip.moduleName}${said}:`,
1293
+ running !== undefined ? ` its running pulse ${running.jobId} is being stopped, and what it had done rolled back;` : "",
1294
+ planned > 0 ? ` its ${planned} planned pulse${planned === 1 ? "" : "s"} will not start.` : " nothing more is planned there.",
1295
+ " Pulses that landed stay.",
1296
+ above.length > 0 ? ` ${above.join(", ")} ${above.length === 1 ? "waits" : "wait"} for it.` : "",
1297
+ ` It runs again only when asked for - regenerate ${basename(tip.plainFile)} - a spec change does not start it.`,
1298
+ ].join("");
1299
+ },
1212
1300
  status() {
1213
1301
  return order.map((key) => statusOf(modules.get(key)));
1214
1302
  },
@@ -142,7 +142,7 @@ export function createDeployTools(options) {
142
142
  },
143
143
  });
144
144
  // The one halt: with the pulses in the conversation, and without them for the release agent.
145
- const stopTool = haltTool(options.jobs !== undefined ? { jobs: options.jobs } : {});
145
+ const stopTool = haltTool(options.jobs !== undefined ? { jobs: options.jobs, ...(options.workLoop !== undefined ? { workLoop: options.workLoop } : {}) } : {});
146
146
  return [deploy, startTool, stopTool];
147
147
  }
148
148
  export const DEPLOY_TOOLS = ["release", "activate", "halt"];
@@ -88,7 +88,8 @@ export function createSpecDiffTools(options) {
88
88
  owed = false;
89
89
  liveReview?.notice(`${params.plain_file}, after the change to ${touched.join(", ")}`);
90
90
  // In the files, the change is work: the loop works out what is left to build and plans it.
91
- const statuses = await loop.bringUp(params.plain_file, params.guidance !== undefined ? { guidance: params.guidance } : {});
91
+ // A spec change, not a request: a module the person halted stays halted.
92
+ const statuses = await loop.bringUp(params.plain_file, { specChange: true, ...(params.guidance !== undefined ? { guidance: params.guidance } : {}) });
92
93
  const lines = [
93
94
  result === "applied" ? `Applied the change to ${touched.join(", ")}.` : `The change to ${touched.join(", ")} was already applied.`,
94
95
  `Building in the background:\n${describeModules(statuses)}\n`,
@@ -40,6 +40,9 @@ on npm runs JavaScript compiled from them instead (`npm run build`, into `dist/`
40
40
  before they pack), as Node runs no `.ts` file inside a `node_modules`. `bin/turri.mjs` runs `src/` where there is one,
41
41
  and `dist/` otherwise.
42
42
 
43
+ To update, `git pull` and `npm install` - the second picks up a dependency that changed, pi's say. `npm link` needs
44
+ running only once. A **turri** already running keeps the code it started with: start it again to run what you pulled.
45
+
43
46
  ## In Docker
44
47
 
45
48
  `docker/Dockerfile` builds an image with Node, git, bash, bubblewrap and **turri** linked; the agents' commands and the
@@ -380,6 +380,18 @@ halts its codebase: nothing more runs there until the spec changes or the module
380
380
  the command line, `turri regenerate` runs until nothing more can start, one line as each job starts and
381
381
  ends, and exits 1 when any failed.
382
382
 
383
+ The person halts a module's planned work too: `halt` (and `/halt`) given a planned row - `planned-<module>-2`,
384
+ `planned-<module>-waiting` - or the module's name. A module's pulses are planned again from its spec at every look, so
385
+ one planned pulse cannot be taken out alone: the module is halted - its running pulse stopped and rolled back, none of
386
+ its planned pulses started, the modules built on it waiting for it - and what landed stays. A halt the person asked for
387
+ holds through spec changes - `write_spec_diff` brings the spec in without starting the module - until the module is
388
+ asked for again (`regenerate`, `conform`). To build some of it later, or cut it differently, there is `steer`; to drop
389
+ work, the spec.
390
+
391
+ A module the loop looked after whose `.plain` file is gone is forgotten - no rows, nothing waited for - and a codebase
392
+ left behind, or whose spec no longer parses, shows nothing planned: its plan, and what it waited for, were the spec's
393
+ before.
394
+
383
395
  ### How the work is cut into jobs
384
396
 
385
397
  A job that renders the whole of a new spec is too big to build well, and a render per functional spec,
package/docs/using.md CHANGED
@@ -58,7 +58,7 @@ where it stopped. A project can have skills of its own, in `.turri/skills/` or `
58
58
  | `validate` | validate a spec, regenerating nothing |
59
59
  | `engine` | show or choose the model the code is regenerated on - see [below](#choosing-a-model) |
60
60
  | `pulses` | the pulses - running, planned and finished - or one of them |
61
- | `halt` | halt a pulse, or what `activate` started |
61
+ | `halt` | halt a pulse, a module's planned pulses, or what `activate` started |
62
62
  | `diagnose` | why a pulse did not land, from its transcript: how it ended, the stages that went wrong, the last failing test run, the last review, the tool calls that failed |
63
63
  | `lineage` | a module's lineage: what each regeneration and conformance run changed in it, and what it was regenerated from |
64
64
  | `cultivate` | run a gated skill, its steps and review gates enforced |
@@ -251,8 +251,9 @@ come waits for a free slot, and says so.
251
251
  Functionalities named after the module - `/regenerate shorty 3 5` - are regenerated though their spec did not
252
252
  change, on the model chosen for functionalities: as `turri regenerate shorty.plain 3 5` does, and the chat's
253
253
  `regenerate` with `functionalities`.
254
- - **`/halt <id>`** halts a pulse - a queued one never starts - and **`/halt activated <name>`** halts a
255
- server `activate` started; it lists both when no id is given.
254
+ - **`/halt <id>`** halts a pulse - a queued one never starts - **`/halt <module>`** (or a planned row's id) halts a
255
+ module's planned pulses until you ask for it again, a spec change not starting it, and **`/halt activated <name>`**
256
+ halts a server `activate` started; it lists all three when no id is given.
256
257
  - **What these commands show you, the chat knows.** What `/regenerate` and `/halt activated` tell you - a spec that
257
258
  does not parse, the pulses planned, a server halted - stays in the chat under the command, and since a slash command
258
259
  runs outside the conversation, it is put in the conversation as well: say "fix it" next and turri knows what you are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4-next.318a98a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {