flanders 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ai/AiRunner.d.ts +0 -1
- package/lib/ai/AiRunner.js +1 -4
- package/lib/ai/AiSession.d.ts +0 -1
- package/lib/ai/AiSession.js +0 -1
- package/lib/ai/ClaudeAdapter.js +1 -4
- package/lib/ai/CodexAdapter.js +7 -11
- package/lib/ai/ToolAdapter.d.ts +1 -7
- package/lib/commands/Implement.d.ts +0 -5
- package/lib/commands/Implement.js +33 -97
- package/lib/plan/PlanFile.d.ts +2 -0
- package/lib/plan/PlanFile.js +32 -18
- package/lib/prompts/prompts.d.ts +1 -3
- package/lib/prompts/prompts.js +10 -60
- package/lib/ui/BottomBlock.d.ts +1 -3
- package/lib/ui/BottomBlock.js +3 -5
- package/lib/ui/formatters.d.ts +0 -1
- package/lib/ui/formatters.js +1 -5
- package/package.json +2 -2
package/lib/ai/AiRunner.d.ts
CHANGED
package/lib/ai/AiRunner.js
CHANGED
|
@@ -18,10 +18,7 @@ async function run(args) {
|
|
|
18
18
|
const base = { prompt, model, effort, abortSignal, onUsage: callbacks.onUsage };
|
|
19
19
|
let invokeArgs;
|
|
20
20
|
if (firstInvocation) {
|
|
21
|
-
if (args.
|
|
22
|
-
invokeArgs = { ...base, forkParentSessionId: args.forkParentSessionId };
|
|
23
|
-
}
|
|
24
|
-
else if (args.resumeSessionId) {
|
|
21
|
+
if (args.resumeSessionId) {
|
|
25
22
|
invokeArgs = { ...base, resumeSessionId: args.resumeSessionId };
|
|
26
23
|
}
|
|
27
24
|
else {
|
package/lib/ai/AiSession.d.ts
CHANGED
package/lib/ai/AiSession.js
CHANGED
|
@@ -72,7 +72,6 @@ class AiSession {
|
|
|
72
72
|
model: this._options.model,
|
|
73
73
|
effort: this._options.effort,
|
|
74
74
|
...(this._options.resumeSessionId != null ? { resumeSessionId: this._options.resumeSessionId } : null),
|
|
75
|
-
...(this._options.forkParentSessionId != null ? { forkParentSessionId: this._options.forkParentSessionId } : null),
|
|
76
75
|
abortSignal: controller.signal,
|
|
77
76
|
callbacks: {
|
|
78
77
|
onOutput,
|
package/lib/ai/ClaudeAdapter.js
CHANGED
|
@@ -167,16 +167,13 @@ class ClaudeAdapterIterator {
|
|
|
167
167
|
if (this._args.resumeSessionId) {
|
|
168
168
|
argv.push("--resume", this._args.resumeSessionId);
|
|
169
169
|
}
|
|
170
|
-
else if (this._args.forkParentSessionId) {
|
|
171
|
-
argv.push("--resume", this._args.forkParentSessionId, "--fork-session");
|
|
172
|
-
}
|
|
173
170
|
if (this._args.model) {
|
|
174
171
|
argv.push("--model", this._args.model);
|
|
175
172
|
}
|
|
176
173
|
if (this._args.effort) {
|
|
177
174
|
argv.push("--effort", this._args.effort);
|
|
178
175
|
}
|
|
179
|
-
argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print");
|
|
176
|
+
argv.push("--input-format", "stream-json", "--output-format", "stream-json", "--include-partial-messages", "--verbose", "--print", "--dangerously-skip-permissions");
|
|
180
177
|
return argv;
|
|
181
178
|
}
|
|
182
179
|
_handleLine(line) {
|
package/lib/ai/CodexAdapter.js
CHANGED
|
@@ -98,15 +98,14 @@ class CodexAdapterIterator {
|
|
|
98
98
|
this._sawTurnCompleted = false;
|
|
99
99
|
this._receivedAnyEvent = false;
|
|
100
100
|
this._fallbackAttempted = false;
|
|
101
|
-
this.
|
|
101
|
+
this._usedResume = false;
|
|
102
102
|
this._start(false);
|
|
103
103
|
}
|
|
104
104
|
_start(useExecFallback) {
|
|
105
105
|
var _a, _b, _c;
|
|
106
106
|
const isResume = !useExecFallback && !!this._args.resumeSessionId;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const argv = this._buildArgv(isResume, isFork);
|
|
107
|
+
this._usedResume = isResume;
|
|
108
|
+
const argv = this._buildArgv(isResume);
|
|
110
109
|
const spawnOptions = { stdio: "pipe" };
|
|
111
110
|
const proc = this._contexts.script.spawn("codex", argv, spawnOptions);
|
|
112
111
|
this._proc = proc;
|
|
@@ -149,18 +148,18 @@ class CodexAdapterIterator {
|
|
|
149
148
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
150
149
|
return;
|
|
151
150
|
}
|
|
152
|
-
if (this.
|
|
151
|
+
if (this._usedResume && !this._receivedAnyEvent && !this._fallbackAttempted) {
|
|
153
152
|
this._fallbackAttempted = true;
|
|
154
153
|
this._queue.push({
|
|
155
154
|
type: "output",
|
|
156
155
|
title: "Continuity lost",
|
|
157
156
|
subtitle: "",
|
|
158
|
-
details: "codex resume
|
|
157
|
+
details: "codex resume unavailable in installed CLI"
|
|
159
158
|
});
|
|
160
159
|
this._cleanup();
|
|
161
160
|
this._sawTurnCompleted = false;
|
|
162
161
|
this._receivedAnyEvent = false;
|
|
163
|
-
this.
|
|
162
|
+
this._usedResume = false;
|
|
164
163
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
165
164
|
this._start(true);
|
|
166
165
|
this._wake();
|
|
@@ -201,14 +200,11 @@ class CodexAdapterIterator {
|
|
|
201
200
|
this._args.abortSignal.addEventListener("abort", this._abortListener, { once: true });
|
|
202
201
|
}
|
|
203
202
|
}
|
|
204
|
-
_buildArgv(isResume
|
|
203
|
+
_buildArgv(isResume) {
|
|
205
204
|
const argv = [];
|
|
206
205
|
if (isResume) {
|
|
207
206
|
argv.push("resume", this._args.resumeSessionId);
|
|
208
207
|
}
|
|
209
|
-
else if (isFork) {
|
|
210
|
-
argv.push("fork", this._args.forkParentSessionId);
|
|
211
|
-
}
|
|
212
208
|
else {
|
|
213
209
|
argv.push("exec");
|
|
214
210
|
}
|
package/lib/ai/ToolAdapter.d.ts
CHANGED
|
@@ -34,17 +34,11 @@ type ToolAdapterInvokeArgsBase = Readonly<{
|
|
|
34
34
|
}>;
|
|
35
35
|
export type ToolAdapterInvokeArgsFresh = ToolAdapterInvokeArgsBase & Readonly<{
|
|
36
36
|
resumeSessionId?: undefined;
|
|
37
|
-
forkParentSessionId?: undefined;
|
|
38
37
|
}>;
|
|
39
38
|
export type ToolAdapterInvokeArgsResume = ToolAdapterInvokeArgsBase & Readonly<{
|
|
40
39
|
resumeSessionId: string;
|
|
41
|
-
forkParentSessionId?: undefined;
|
|
42
40
|
}>;
|
|
43
|
-
export type
|
|
44
|
-
resumeSessionId?: undefined;
|
|
45
|
-
forkParentSessionId: string;
|
|
46
|
-
}>;
|
|
47
|
-
export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume | ToolAdapterInvokeArgsFork;
|
|
41
|
+
export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume;
|
|
48
42
|
export interface ToolAdapter {
|
|
49
43
|
invoke(args: ToolAdapterInvokeArgs): AsyncIterable<ToolEvent>;
|
|
50
44
|
}
|
|
@@ -27,7 +27,6 @@ export declare class Implement {
|
|
|
27
27
|
private _block;
|
|
28
28
|
private _buffered;
|
|
29
29
|
private _currentWorkerSessionId;
|
|
30
|
-
private _currentPrepSessionId;
|
|
31
30
|
private _activeSession;
|
|
32
31
|
private _activeScript;
|
|
33
32
|
private _activeReviewerSessions;
|
|
@@ -42,7 +41,6 @@ export declare class Implement {
|
|
|
42
41
|
private _taskRateLimitMs;
|
|
43
42
|
private _taskRateLimitStartedAt;
|
|
44
43
|
private _taskTokens;
|
|
45
|
-
private _restingFooterKind;
|
|
46
44
|
private _runPromise;
|
|
47
45
|
get config(): FlandersConfig | null;
|
|
48
46
|
constructor(rawArgs: readonly string[], _options: ImplementOptions, _contexts: ImplementContexts);
|
|
@@ -54,11 +52,8 @@ export declare class Implement {
|
|
|
54
52
|
private _activeSeconds;
|
|
55
53
|
private _persistMetrics;
|
|
56
54
|
private _updateMetrics;
|
|
57
|
-
private _reviewerMatchesWorker;
|
|
58
|
-
private _prepActive;
|
|
59
55
|
private _gitOutputContext;
|
|
60
56
|
private _runTask;
|
|
61
|
-
private _prepStage;
|
|
62
57
|
private _workerStage;
|
|
63
58
|
private _appendLinkedContent;
|
|
64
59
|
private _buildStage;
|
|
@@ -61,7 +61,6 @@ class Implement {
|
|
|
61
61
|
this._workspace = null;
|
|
62
62
|
this._block = null;
|
|
63
63
|
this._currentWorkerSessionId = null;
|
|
64
|
-
this._currentPrepSessionId = null;
|
|
65
64
|
this._activeSession = null;
|
|
66
65
|
this._activeScript = null;
|
|
67
66
|
this._activeReviewerSessions = new Set();
|
|
@@ -76,7 +75,6 @@ class Implement {
|
|
|
76
75
|
this._taskRateLimitMs = 0;
|
|
77
76
|
this._taskRateLimitStartedAt = null;
|
|
78
77
|
this._taskTokens = { it: 0, ot: 0 };
|
|
79
|
-
this._restingFooterKind = "working";
|
|
80
78
|
this._runPromise = this._run(rawArgs);
|
|
81
79
|
this._runPromise.catch(() => { });
|
|
82
80
|
}
|
|
@@ -185,7 +183,7 @@ class Implement {
|
|
|
185
183
|
const refreshed = plan.parse();
|
|
186
184
|
const completedSoFar = refreshed.tasks.filter(t => t.done).length;
|
|
187
185
|
const indexLabel = `${completedSoFar + 1}/${totalTasks}`;
|
|
188
|
-
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel
|
|
186
|
+
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel);
|
|
189
187
|
if (this._disposed) {
|
|
190
188
|
this._finalizeBlock("Interrupted");
|
|
191
189
|
return 1;
|
|
@@ -244,12 +242,12 @@ class Implement {
|
|
|
244
242
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList));
|
|
245
243
|
await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
246
244
|
}
|
|
247
|
-
_setActivity(activity
|
|
245
|
+
_setActivity(activity) {
|
|
248
246
|
if (!this._currentTask)
|
|
249
247
|
return;
|
|
250
248
|
this._block.setHeader({
|
|
251
249
|
indexLabel: this._currentIndexLabel,
|
|
252
|
-
iteration:
|
|
250
|
+
iteration: this._currentIteration,
|
|
253
251
|
activity,
|
|
254
252
|
taskNumber: this._currentTask.taskNumber || undefined,
|
|
255
253
|
title: this._currentTask.title
|
|
@@ -275,13 +273,6 @@ class Implement {
|
|
|
275
273
|
plan: { tokens: planTotals.it + planTotals.ot, seconds: planTotals.t }
|
|
276
274
|
});
|
|
277
275
|
}
|
|
278
|
-
_reviewerMatchesWorker(reviewer) {
|
|
279
|
-
const w = this._config.worker;
|
|
280
|
-
return reviewer.tool === w.tool && reviewer.model === w.model && reviewer.effort === w.effort;
|
|
281
|
-
}
|
|
282
|
-
_prepActive() {
|
|
283
|
-
return this._config.reviewers.some(r => this._reviewerMatchesWorker(r));
|
|
284
|
-
}
|
|
285
276
|
_gitOutputContext() {
|
|
286
277
|
return {
|
|
287
278
|
write: text => this._buffered.write(text),
|
|
@@ -291,31 +282,15 @@ class Implement {
|
|
|
291
282
|
onResize: listener => this._contexts.output.onResize(listener)
|
|
292
283
|
};
|
|
293
284
|
}
|
|
294
|
-
async _runTask(plan, task, ws, indexLabel
|
|
285
|
+
async _runTask(plan, task, ws, indexLabel) {
|
|
295
286
|
this._currentTask = task;
|
|
296
287
|
this._currentIndexLabel = indexLabel;
|
|
297
288
|
this._currentWorkerSessionId = null;
|
|
298
|
-
this._currentPrepSessionId = null;
|
|
299
289
|
this._taskStartedAt = this._contexts.time.now();
|
|
300
290
|
this._taskRateLimitMs = 0;
|
|
301
291
|
this._taskRateLimitStartedAt = null;
|
|
302
292
|
this._taskTokens = { it: 0, ot: 0 };
|
|
303
293
|
this._updateMetrics(plan);
|
|
304
|
-
const prepActive = this._prepActive();
|
|
305
|
-
if (prepActive) {
|
|
306
|
-
this._setActivity("preparing", null);
|
|
307
|
-
this._restingFooterKind = "preparing";
|
|
308
|
-
this._block.setFooter({ kind: "preparing" });
|
|
309
|
-
const prepOk = await this._prepStage(plan, task, ws, taskIndex);
|
|
310
|
-
if (this._disposed) {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
if (!prepOk) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
this._restingFooterKind = "working";
|
|
317
|
-
this._block.setFooter({ kind: "working" });
|
|
318
|
-
}
|
|
319
294
|
let iteration = 0;
|
|
320
295
|
for (;;) {
|
|
321
296
|
iteration++;
|
|
@@ -329,7 +304,7 @@ class Implement {
|
|
|
329
304
|
return false;
|
|
330
305
|
}
|
|
331
306
|
this._setActivity("implementing");
|
|
332
|
-
const workerOk = await this._workerStage(plan, task, ws, iteration
|
|
307
|
+
const workerOk = await this._workerStage(plan, task, ws, iteration);
|
|
333
308
|
if (this._disposed) {
|
|
334
309
|
return false;
|
|
335
310
|
}
|
|
@@ -358,7 +333,7 @@ class Implement {
|
|
|
358
333
|
continue;
|
|
359
334
|
}
|
|
360
335
|
this._setActivity("reviewing");
|
|
361
|
-
const reviewOk = await this._reviewerStage(plan, task, ws, iteration
|
|
336
|
+
const reviewOk = await this._reviewerStage(plan, task, ws, iteration);
|
|
362
337
|
if (this._disposed) {
|
|
363
338
|
return false;
|
|
364
339
|
}
|
|
@@ -387,52 +362,25 @@ class Implement {
|
|
|
387
362
|
return true;
|
|
388
363
|
}
|
|
389
364
|
}
|
|
390
|
-
async
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.
|
|
394
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
395
|
-
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
396
|
-
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
397
|
-
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
398
|
-
try {
|
|
399
|
-
const { result, capturedOutput } = await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
400
|
-
this._taskTokens.it += result.inputTokens;
|
|
401
|
-
this._taskTokens.ot += result.outputTokens;
|
|
402
|
-
await this._persistMetrics(plan, task.line);
|
|
403
|
-
this._updateMetrics(plan);
|
|
404
|
-
await this._writeLog(ws.prepLog(taskIndex), capturedOutput);
|
|
405
|
-
if (result.sessionId === null) {
|
|
406
|
-
this._workspace.preserveOnDispose();
|
|
407
|
-
await this._writeErrorLog(ws, `prep returned no session id for task at line ${task.line} ("${task.title}")`);
|
|
408
|
-
this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") returned no session id. Inspect logs at ${ws.root}.\n`);
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
this._currentPrepSessionId = result.sessionId;
|
|
412
|
-
return true;
|
|
365
|
+
async _workerStage(plan, task, ws, iteration) {
|
|
366
|
+
let taskText;
|
|
367
|
+
if (iteration === 1) {
|
|
368
|
+
taskText = plan.fullTaskText(task);
|
|
413
369
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") failed. Inspect logs at ${ws.root}.\n`);
|
|
420
|
-
return false;
|
|
370
|
+
else if (this._currentWorkerSessionId !== null) {
|
|
371
|
+
taskText = "(The full task text and the contracts and rules it references were provided when this session began and remain available to you through session continuity; they are not repeated here.)";
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
taskText = `(Your previous session for this task could not be resumed, so this is a fresh invocation. You are continuing work on the task at line ${task.line} of the plan file, titled "${task.title}". Re-read that task and the contracts and rules it references directly from the files on disk — they are not re-injected here — and address the previous-iteration briefing below.)`;
|
|
421
375
|
}
|
|
422
|
-
}
|
|
423
|
-
async _workerStage(plan, task, ws, iteration, prepActive) {
|
|
424
376
|
let prompt = prompts_1.prompts.worker
|
|
425
377
|
.split("<PLAN_PATH>").join(plan.path)
|
|
426
|
-
.split("<
|
|
427
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
378
|
+
.split("<TASK_TEXT>").join(taskText)
|
|
428
379
|
.split("<BUILD_SCRIPT_PATH>").join(ws.buildScript)
|
|
429
380
|
.split("<TEST_SCRIPT_PATH>").join(ws.testScript)
|
|
430
381
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
431
382
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
432
383
|
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
433
|
-
if (iteration === 1 && !prepActive) {
|
|
434
|
-
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
435
|
-
}
|
|
436
384
|
if (iteration > 1) {
|
|
437
385
|
const briefing = prompts_1.prompts.previousIterationBriefing
|
|
438
386
|
.split("<ITERATION>").join(String(iteration))
|
|
@@ -440,10 +388,11 @@ class Implement {
|
|
|
440
388
|
prompt = `${prompt}\n\n${briefing}`;
|
|
441
389
|
}
|
|
442
390
|
try {
|
|
391
|
+
if (iteration === 1) {
|
|
392
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
393
|
+
}
|
|
443
394
|
const { result, capturedOutput } = iteration === 1
|
|
444
|
-
?
|
|
445
|
-
? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, null, this._currentPrepSessionId)
|
|
446
|
-
: await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
|
|
395
|
+
? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
|
|
447
396
|
: await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, this._currentWorkerSessionId);
|
|
448
397
|
if (result.sessionId !== null) {
|
|
449
398
|
this._currentWorkerSessionId = result.sessionId;
|
|
@@ -471,15 +420,10 @@ class Implement {
|
|
|
471
420
|
const sections = [];
|
|
472
421
|
for (const relPath of allPaths) {
|
|
473
422
|
const absPath = (0, fsUtils_1.joinPath)(this._options.projectRoot, relPath);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
sections.push(`## ${relPath}\n\n${content}`);
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
sections.push(`## ${relPath}\n\n(file not found)`);
|
|
480
|
-
}
|
|
423
|
+
const content = await this._contexts.fs.readFile(absPath);
|
|
424
|
+
sections.push(`## ${relPath}\n\n${content}`);
|
|
481
425
|
}
|
|
482
|
-
return `${prompt}\n\n## Linked reference content\n\n${sections.join("\n\n")}`;
|
|
426
|
+
return `${prompt}\n\n## Linked reference content\n\nThe full content of every contract and rule this task references is included below, so you do not need to open these files for this iteration:\n\n${sections.join("\n\n")}`;
|
|
483
427
|
}
|
|
484
428
|
async _buildStage(plan, taskLine, ws, iteration) {
|
|
485
429
|
if (!(await (0, fsUtils_1.isNonEmptyFile)(this._contexts.fs, ws.buildScript))) {
|
|
@@ -557,7 +501,7 @@ class Implement {
|
|
|
557
501
|
}
|
|
558
502
|
}
|
|
559
503
|
}
|
|
560
|
-
async _reviewerStage(plan, task, ws, iteration
|
|
504
|
+
async _reviewerStage(plan, task, ws, iteration) {
|
|
561
505
|
const reviewers = this._config.reviewers;
|
|
562
506
|
this._reviewerStates = reviewers.map(r => ({ tool: r.tool, model: r.model, effort: r.effort, state: "running" }));
|
|
563
507
|
this._reviewerLogicalStatuses = reviewers.map(() => "running");
|
|
@@ -569,7 +513,7 @@ class Implement {
|
|
|
569
513
|
await this._workspace.clearErrorLog();
|
|
570
514
|
let failureCaught = null;
|
|
571
515
|
const outcomes = reviewers.map(() => "cancelled");
|
|
572
|
-
const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration,
|
|
516
|
+
const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx)
|
|
573
517
|
.then(outcome => { outcomes[idx] = outcome; })
|
|
574
518
|
.catch(e => {
|
|
575
519
|
if (failureCaught === null) {
|
|
@@ -599,21 +543,16 @@ class Implement {
|
|
|
599
543
|
await this._workspace.writeErrorLog(aggregate);
|
|
600
544
|
return false;
|
|
601
545
|
}
|
|
602
|
-
async _runOneReviewerToVerdict(plan, task, ws, iteration,
|
|
546
|
+
async _runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx) {
|
|
603
547
|
const reviewerNum = idx + 1;
|
|
604
|
-
const matchesWorker = this._reviewerMatchesWorker(reviewer);
|
|
605
|
-
const useBranchA = prepActive && matchesWorker;
|
|
606
548
|
let prompt = prompts_1.prompts.reviewer
|
|
607
549
|
.split("<PLAN_PATH>").join(plan.path)
|
|
608
|
-
.split("<
|
|
609
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
550
|
+
.split("<TASK_TEXT>").join(plan.fullTaskText(task))
|
|
610
551
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
611
552
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
612
553
|
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList))
|
|
613
554
|
.split("<ERROR_LOG_PATH>").join(ws.reviewerErrorLog(reviewerNum));
|
|
614
|
-
|
|
615
|
-
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
616
|
-
}
|
|
555
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
617
556
|
const controller = new AbortController();
|
|
618
557
|
this._reviewerAbortControllers.set(idx, controller);
|
|
619
558
|
try {
|
|
@@ -656,9 +595,7 @@ class Implement {
|
|
|
656
595
|
};
|
|
657
596
|
let runResult;
|
|
658
597
|
try {
|
|
659
|
-
runResult =
|
|
660
|
-
? await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, this._currentPrepSessionId, callbacks)
|
|
661
|
-
: await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, null, callbacks);
|
|
598
|
+
runResult = await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, callbacks);
|
|
662
599
|
}
|
|
663
600
|
catch (e) {
|
|
664
601
|
if (this._cancelledReviewers.has(idx)) {
|
|
@@ -726,7 +663,7 @@ class Implement {
|
|
|
726
663
|
}
|
|
727
664
|
if (this._disposed)
|
|
728
665
|
return;
|
|
729
|
-
this._block.setFooter({ kind:
|
|
666
|
+
this._block.setFooter({ kind: "working" });
|
|
730
667
|
},
|
|
731
668
|
register: (session) => {
|
|
732
669
|
this._activeSession = { session };
|
|
@@ -739,10 +676,10 @@ class Implement {
|
|
|
739
676
|
}
|
|
740
677
|
};
|
|
741
678
|
}
|
|
742
|
-
_runAi(tool, model, effort, prompt, initialSessionId
|
|
743
|
-
return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null,
|
|
679
|
+
_runAi(tool, model, effort, prompt, initialSessionId) {
|
|
680
|
+
return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null, this._defaultRunAiCallbacks());
|
|
744
681
|
}
|
|
745
|
-
async _runAiWith(tool, model, effort, prompt, initialSessionId,
|
|
682
|
+
async _runAiWith(tool, model, effort, prompt, initialSessionId, callbacks) {
|
|
746
683
|
if (this._disposed) {
|
|
747
684
|
throw new Error("Implement disposed");
|
|
748
685
|
}
|
|
@@ -767,7 +704,6 @@ class Implement {
|
|
|
767
704
|
model,
|
|
768
705
|
effort,
|
|
769
706
|
...(initialSessionId != null ? { resumeSessionId: initialSessionId } : null),
|
|
770
|
-
...(forkFromSessionId != null ? { forkParentSessionId: forkFromSessionId } : null),
|
|
771
707
|
onLongWaitStart: callbacks.onLongWaitStart,
|
|
772
708
|
onLongWaitEnd: callbacks.onLongWaitEnd
|
|
773
709
|
}, {
|
package/lib/plan/PlanFile.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type PlanParseResult = Readonly<{
|
|
|
27
27
|
}>;
|
|
28
28
|
export declare const TASK_LINE: RegExp;
|
|
29
29
|
export declare function parsePlan(content: string): PlanParseResult;
|
|
30
|
+
export declare function extractFullTaskText(content: string, taskLineNumber: number): string;
|
|
30
31
|
export declare function parseLinkedPaths(content: string, taskLineNumber: number): TaskLinkedPaths;
|
|
31
32
|
export declare class PlanFile {
|
|
32
33
|
readonly path: string;
|
|
@@ -40,6 +41,7 @@ export declare class PlanFile {
|
|
|
40
41
|
markDone(lineNumber: number, metrics: TaskMetrics): Promise<void>;
|
|
41
42
|
markOpen(lineNumber: number, metrics: TaskMetrics): Promise<void>;
|
|
42
43
|
linkedPaths(task: PlanTask): TaskLinkedPaths;
|
|
44
|
+
fullTaskText(task: PlanTask): string;
|
|
43
45
|
planTotals(): TaskMetrics;
|
|
44
46
|
private _rewriteTaskLine;
|
|
45
47
|
}
|
package/lib/plan/PlanFile.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PlanFile = exports.TASK_LINE = void 0;
|
|
4
4
|
exports.parsePlan = parsePlan;
|
|
5
|
+
exports.extractFullTaskText = extractFullTaskText;
|
|
5
6
|
exports.parseLinkedPaths = parseLinkedPaths;
|
|
6
7
|
exports.TASK_LINE = /^(\s*[-*+]\s+)\[([ xX])\](\{[^}]*\})(\s.*)?$/;
|
|
7
8
|
const MALFORMED_TASK_LINE = /^\s*[-*+]\s+\[[^\]]*\]\{/;
|
|
@@ -88,14 +89,12 @@ function parsePlan(content) {
|
|
|
88
89
|
size: content.length
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return paths;
|
|
92
|
+
const MARKDOWN_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
93
|
+
const CONTRACTS_SEGMENT = ".spec/contracts/";
|
|
94
|
+
const RULES_SEGMENT = ".spec/rules/";
|
|
95
|
+
function detectNewline(content) {
|
|
96
|
+
const newlineMatch = /\r\n|\n/.exec(content);
|
|
97
|
+
return newlineMatch ? newlineMatch[0] : "\n";
|
|
99
98
|
}
|
|
100
99
|
function taskBodyLines(content, taskLineNumber) {
|
|
101
100
|
const lines = content.split(/\r?\n/);
|
|
@@ -108,17 +107,30 @@ function taskBodyLines(content, taskLineNumber) {
|
|
|
108
107
|
}
|
|
109
108
|
return body;
|
|
110
109
|
}
|
|
110
|
+
function extractFullTaskText(content, taskLineNumber) {
|
|
111
|
+
const lines = content.split(/\r?\n/);
|
|
112
|
+
const taskLine = lines[taskLineNumber - 1];
|
|
113
|
+
const body = taskBodyLines(content, taskLineNumber);
|
|
114
|
+
return [taskLine, ...body].join(detectNewline(content));
|
|
115
|
+
}
|
|
111
116
|
function parseLinkedPaths(content, taskLineNumber) {
|
|
112
117
|
const body = taskBodyLines(content, taskLineNumber);
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
const contracts = [];
|
|
119
|
+
const rules = [];
|
|
115
120
|
for (const line of body) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
let m;
|
|
122
|
+
while ((m = MARKDOWN_LINK.exec(line)) !== null) {
|
|
123
|
+
const path = m[1].split("#")[0].replace(/^\//, "");
|
|
124
|
+
if (path.includes(CONTRACTS_SEGMENT)) {
|
|
125
|
+
if (!contracts.includes(path)) {
|
|
126
|
+
contracts.push(path);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (path.includes(RULES_SEGMENT)) {
|
|
130
|
+
if (!rules.includes(path)) {
|
|
131
|
+
rules.push(path);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
return { contracts, rules };
|
|
@@ -156,6 +168,9 @@ class PlanFile {
|
|
|
156
168
|
linkedPaths(task) {
|
|
157
169
|
return parseLinkedPaths(this._content, task.line);
|
|
158
170
|
}
|
|
171
|
+
fullTaskText(task) {
|
|
172
|
+
return extractFullTaskText(this._content, task.line);
|
|
173
|
+
}
|
|
159
174
|
planTotals() {
|
|
160
175
|
const { tasks } = parsePlan(this._content);
|
|
161
176
|
let it = 0, ot = 0, t = 0;
|
|
@@ -168,8 +183,7 @@ class PlanFile {
|
|
|
168
183
|
}
|
|
169
184
|
async _rewriteTaskLine(lineNumber, metrics, flip) {
|
|
170
185
|
validateMetricsInput(metrics);
|
|
171
|
-
const
|
|
172
|
-
const newline = newlineMatch ? newlineMatch[0] : "\n";
|
|
186
|
+
const newline = detectNewline(this._content);
|
|
173
187
|
const lines = this._content.split(/\r?\n/);
|
|
174
188
|
const idx = lineNumber - 1;
|
|
175
189
|
const raw = lines[idx];
|
package/lib/prompts/prompts.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export declare const enum Placeholders {
|
|
2
2
|
PLAN_PATH = "<PLAN_PATH>",
|
|
3
|
-
|
|
4
|
-
TASK_TITLE = "<TASK_TITLE>",
|
|
3
|
+
TASK_TEXT = "<TASK_TEXT>",
|
|
5
4
|
BUILD_SCRIPT_PATH = "<BUILD_SCRIPT_PATH>",
|
|
6
5
|
TEST_SCRIPT_PATH = "<TEST_SCRIPT_PATH>",
|
|
7
6
|
ERROR_LOG_PATH = "<ERROR_LOG_PATH>",
|
|
@@ -43,6 +42,5 @@ export declare const prompts: {
|
|
|
43
42
|
detectBuildAndTest: string;
|
|
44
43
|
worker: string;
|
|
45
44
|
reviewer: string;
|
|
46
|
-
prep: string;
|
|
47
45
|
previousIterationBriefing: string;
|
|
48
46
|
};
|
package/lib/prompts/prompts.js
CHANGED
|
@@ -160,10 +160,11 @@ Spec-folder write boundary: you must not create, modify, delete, or rename any f
|
|
|
160
160
|
${foregroundBoundary}`,
|
|
161
161
|
worker: `You are the worker agent for the Flanders implement iteration loop.
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
The plan file is at ${"<PLAN_PATH>"}; you may open it for broader context.
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
## Your task
|
|
166
|
+
|
|
167
|
+
${"<TASK_TEXT>"}
|
|
167
168
|
|
|
168
169
|
## Adversarial review awaits
|
|
169
170
|
|
|
@@ -178,7 +179,7 @@ Your output will be inspected by an adversarial reviewer immediately after you f
|
|
|
178
179
|
Condition 4 causes most rejections in practice. Rules whose scope matches your changes (testing rules when you touch tests, disposable rules when you touch async resources, UI rules when you change terminal output, etc.) are mandatory whether the task links them or not. Treat the global contract and rule lists below as part of your specification, not as optional reading. The reviewer will also enumerate every occurrence of a pattern violation, not just the first one, so partial compliance within a file is itself a FAIL.
|
|
179
180
|
|
|
180
181
|
Procedure:
|
|
181
|
-
1.
|
|
182
|
+
1. Read the task shown above and respect the obligations of every contract and rule it references exactly. You may consult those files, or the plan file for broader context, at your discretion.
|
|
182
183
|
2. Implement the task. Update or extend tests so the new behavior is covered.
|
|
183
184
|
3. If your implementation changes how the project builds or how its tests run, also update the build and test scripts at:
|
|
184
185
|
- Build script: ${"<BUILD_SCRIPT_PATH>"}
|
|
@@ -232,12 +233,13 @@ Each path below is a behavior rule's namespace. A behavior rule governs how the
|
|
|
232
233
|
${"<BEHAVIOR_RULE_LIST>"}`,
|
|
233
234
|
reviewer: `You are the adversarial reviewer agent for the Flanders implement iteration loop.
|
|
234
235
|
|
|
235
|
-
|
|
236
|
+
The plan file is at ${"<PLAN_PATH>"}; you may open it for broader context, but you do not need to in order to find the task — the full task is provided in this prompt.
|
|
237
|
+
|
|
238
|
+
## The task under review
|
|
236
239
|
|
|
237
|
-
|
|
238
|
-
${"<TASK_TITLE>"}
|
|
240
|
+
${"<TASK_TEXT>"}
|
|
239
241
|
|
|
240
|
-
|
|
242
|
+
The task's full description, its acceptance criteria, and the full content of every contract and rule it references are provided to you directly — the referenced contracts and rules are injected inline at the end of this prompt (under "Linked reference content"). Inspect the working-tree changes that the worker just produced.
|
|
241
243
|
|
|
242
244
|
## Determining the worker's change set
|
|
243
245
|
|
|
@@ -268,58 +270,6 @@ Git boundary: you are an inspection-only agent. You must not execute any git com
|
|
|
268
270
|
Spec-folder write boundary: you must not create, modify, delete, or rename any file inside any \`.spec/contracts\` folder, any \`.spec/rules\` folder, or the \`plans/\` folder. These folders are governed by dedicated skills and the implement command's bounded checkpoint updates; no other agent may write to them. See shared/spec-folder-write-authority.md for the full obligation.
|
|
269
271
|
|
|
270
272
|
${foregroundBoundary}`,
|
|
271
|
-
prep: `You are the prep agent for the Flanders implement iteration loop.
|
|
272
|
-
|
|
273
|
-
Plan file path: ${"<PLAN_PATH>"}
|
|
274
|
-
|
|
275
|
-
The current task is on line ${"<TASK_LINE>"} of that plan file. Its title, verbatim, is:
|
|
276
|
-
${"<TASK_TITLE>"}
|
|
277
|
-
|
|
278
|
-
## Your job
|
|
279
|
-
|
|
280
|
-
Read the task and its reference material so the session is ready to be forked by the worker and reviewer agents. You do not implement anything — you are a read-only context-loading agent.
|
|
281
|
-
|
|
282
|
-
Procedure:
|
|
283
|
-
1. Open the plan file and find the task at the line indicated above. Read its full description, acceptance criteria, and every contract and rule file the task references.
|
|
284
|
-
2. From the global lists below, read the full content of every additional contract or rule you judge relevant to the task, even if the task does not explicitly reference it. Err on the side of loading material that might be needed rather than skipping it.
|
|
285
|
-
|
|
286
|
-
## Read-only obligation
|
|
287
|
-
|
|
288
|
-
You must not implement, modify, or write anything in the project. Do not use Edit, Write, or any Bash command that mutates project state. Your only job is to read and load context.
|
|
289
|
-
|
|
290
|
-
## Spec-folder write boundary
|
|
291
|
-
|
|
292
|
-
You must not write to any \`.spec/contracts\` folder, any \`.spec/rules\` folder, or the \`plans/\` folder. These folders are governed by dedicated skills and the implement command's bounded checkpoint updates; no other agent may create, modify, delete, or rename files in them. See shared/spec-folder-write-authority.md for the full obligation.
|
|
293
|
-
|
|
294
|
-
## Git boundary
|
|
295
|
-
|
|
296
|
-
You must not execute any git command that modifies repository state — no \`git add\`, \`git commit\`, \`git stash\`, \`git reset\`, \`git restore\`, \`git checkout -b\`, \`git branch\`, \`git tag\`, \`git rebase\`, \`git merge\`, \`git cherry-pick\`, no edits under \`.git/\`, and no remote git operations (\`fetch\`, \`pull\`, \`push\`). Read-only git commands (\`git status\`, \`git diff\`, \`git log\`, \`git show\`, \`git blame\`, \`git ls-files\`) are allowed when you need to inspect the repo. The full obligation lives in rules/ai/agents/no-git-writes.md.
|
|
297
|
-
|
|
298
|
-
${foregroundBoundary}
|
|
299
|
-
|
|
300
|
-
## Available contracts
|
|
301
|
-
|
|
302
|
-
Each path below is the contract's namespace. Scan this list and open every contract whose public surface intersects the work in this task.
|
|
303
|
-
|
|
304
|
-
${"<CONTRACT_LIST>"}
|
|
305
|
-
|
|
306
|
-
## Available rules
|
|
307
|
-
|
|
308
|
-
Each path below is the rule's namespace. Scan this list and open every rule whose scope matches the work in this task.
|
|
309
|
-
|
|
310
|
-
${"<RULE_LIST>"}
|
|
311
|
-
|
|
312
|
-
## Available behavior rules
|
|
313
|
-
|
|
314
|
-
Each path below is a behavior rule's namespace. A behavior rule governs how the files and changes Flanders authors are named, placed, and organized within the part of the project tree that the rule's \`.spec/flanders\` folder scopes; every behavior rule whose \`.spec/flanders\` scope encloses the files this task's work touches must be honored. Read all of those in-scope behavior rules now, so the worker and reviewer forked from this session honor them. Like the global contract and rule lists above, in-scope behavior rules are mandatory whether or not the task links them.
|
|
315
|
-
|
|
316
|
-
${"<BEHAVIOR_RULE_LIST>"}
|
|
317
|
-
|
|
318
|
-
## Ending discipline
|
|
319
|
-
|
|
320
|
-
When you have finished reading all relevant material, end your reply with the word READY on its own line and no pending tool calls. The session must be in a forkable state.
|
|
321
|
-
|
|
322
|
-
READY`,
|
|
323
273
|
previousIterationBriefing: `This is iteration ${"<ITERATION>"} for this task. The previous iteration produced a problem to review before retrying. Read the full context written into the error log file at:
|
|
324
274
|
|
|
325
275
|
${"<ERROR_LOG_PATH>"}
|
package/lib/ui/BottomBlock.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type BottomBlockIO = {
|
|
|
6
6
|
columns(): number;
|
|
7
7
|
onResize(listener: () => void): () => void;
|
|
8
8
|
};
|
|
9
|
-
export type Activity = "
|
|
9
|
+
export type Activity = "implementing" | "reviewing" | "building" | "testing";
|
|
10
10
|
export type HeaderFields = {
|
|
11
11
|
indexLabel?: string | null;
|
|
12
12
|
iteration?: number | null;
|
|
@@ -29,8 +29,6 @@ export type FooterState = {
|
|
|
29
29
|
kind: "blank";
|
|
30
30
|
} | {
|
|
31
31
|
kind: "working";
|
|
32
|
-
} | {
|
|
33
|
-
kind: "preparing";
|
|
34
32
|
} | {
|
|
35
33
|
kind: "waiting";
|
|
36
34
|
waitKind: WaitKind;
|
package/lib/ui/BottomBlock.js
CHANGED
|
@@ -66,7 +66,7 @@ class BottomBlock {
|
|
|
66
66
|
return;
|
|
67
67
|
this._cancelTimers();
|
|
68
68
|
this._footer = state;
|
|
69
|
-
if (state.kind === "working"
|
|
69
|
+
if (state.kind === "working") {
|
|
70
70
|
this._animFrame = 0;
|
|
71
71
|
}
|
|
72
72
|
if (this._mounted) {
|
|
@@ -125,7 +125,7 @@ class BottomBlock {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
_startFooterTimer() {
|
|
128
|
-
if (this._footer.kind === "working"
|
|
128
|
+
if (this._footer.kind === "working") {
|
|
129
129
|
this._scheduleAnimTick();
|
|
130
130
|
}
|
|
131
131
|
else if (this._footer.kind === "waiting") {
|
|
@@ -135,7 +135,7 @@ class BottomBlock {
|
|
|
135
135
|
_scheduleAnimTick() {
|
|
136
136
|
this._animTimer = this._time.setTimeout(() => {
|
|
137
137
|
this._animTimer = null;
|
|
138
|
-
if (this._disposed || this._finalized ||
|
|
138
|
+
if (this._disposed || this._finalized || this._footer.kind !== "working")
|
|
139
139
|
return;
|
|
140
140
|
this._animFrame = (this._animFrame + 1) % FRAMES.length;
|
|
141
141
|
this._clearBlock();
|
|
@@ -189,8 +189,6 @@ class BottomBlock {
|
|
|
189
189
|
return "";
|
|
190
190
|
case "working":
|
|
191
191
|
return (0, formatters_1.formatWorkingFooter)(FRAMES[this._animFrame], cols);
|
|
192
|
-
case "preparing":
|
|
193
|
-
return (0, formatters_1.formatPreparingFooter)(FRAMES[this._animFrame], cols);
|
|
194
192
|
case "waiting": {
|
|
195
193
|
const remaining = Math.max(0, this._footer.endTime - this._time.now());
|
|
196
194
|
const dateStr = (0, formatters_1.formatDateTime)(new Date(this._footer.endTime));
|
package/lib/ui/formatters.d.ts
CHANGED
|
@@ -39,5 +39,4 @@ export type ReviewerEntry = {
|
|
|
39
39
|
};
|
|
40
40
|
export declare function formatReviewingFooter(reviewers: readonly ReviewerEntry[], cols: number): string;
|
|
41
41
|
export declare function formatWorkingFooter(frame: string, cols: number): string;
|
|
42
|
-
export declare function formatPreparingFooter(frame: string, cols: number): string;
|
|
43
42
|
export declare function formatWaitingFooter(heading: string, dateTime: string, countdown: string, cols: number): string;
|
package/lib/ui/formatters.js
CHANGED
|
@@ -17,7 +17,6 @@ exports.formatSnapshotMetrics = formatSnapshotMetrics;
|
|
|
17
17
|
exports.formatSnapshotBlock = formatSnapshotBlock;
|
|
18
18
|
exports.formatReviewingFooter = formatReviewingFooter;
|
|
19
19
|
exports.formatWorkingFooter = formatWorkingFooter;
|
|
20
|
-
exports.formatPreparingFooter = formatPreparingFooter;
|
|
21
20
|
exports.formatWaitingFooter = formatWaitingFooter;
|
|
22
21
|
exports.CYAN = "\x1b[36m";
|
|
23
22
|
exports.YELLOW = "\x1b[33m";
|
|
@@ -114,7 +113,7 @@ function formatActiveTime(seconds) {
|
|
|
114
113
|
const m = Math.floor((s % 3600) / 60);
|
|
115
114
|
return `${h}h${pad2(m)}m${pad2(s % 60)}s`;
|
|
116
115
|
}
|
|
117
|
-
const LIVE_ACTIVITIES = new Set(["
|
|
116
|
+
const LIVE_ACTIVITIES = new Set(["implementing", "reviewing", "building", "testing"]);
|
|
118
117
|
function formatHeaderLine(indexLabel, iteration, activity, taskNumber, title, cols) {
|
|
119
118
|
const segments = [];
|
|
120
119
|
if (indexLabel != null) {
|
|
@@ -276,9 +275,6 @@ function fitOrangeFooterLine(text, cols) {
|
|
|
276
275
|
function formatWorkingFooter(frame, cols) {
|
|
277
276
|
return fitOrangeFooterLine(`${frame} Working`, cols);
|
|
278
277
|
}
|
|
279
|
-
function formatPreparingFooter(frame, cols) {
|
|
280
|
-
return fitOrangeFooterLine(`${frame} Preparing`, cols);
|
|
281
|
-
}
|
|
282
278
|
function formatWaitingFooter(heading, dateTime, countdown, cols) {
|
|
283
279
|
return fitOrangeFooterLine(`${heading} — ${dateTime} — ${countdown}`, cols);
|
|
284
280
|
}
|
package/package.json
CHANGED