castle-web-cli 0.4.35 → 0.4.37
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/dist/agent-prompts.js +10 -12
- package/dist/agent.js +40 -41
- package/dist/chat-client.js +4 -1
- package/package.json +1 -1
package/dist/agent-prompts.js
CHANGED
|
@@ -20,7 +20,6 @@ Hard rules:
|
|
|
20
20
|
\`\`\`castle-task
|
|
21
21
|
short imperative title on the first line
|
|
22
22
|
after: comma-separated titles or ids this task must wait for (optional line)
|
|
23
|
-
supersedes: comma-separated titles or ids this task replaces (optional line)
|
|
24
23
|
Then a SHORT self-contained prompt -- one tight paragraph (aim under 100
|
|
25
24
|
words): what to build or fix and what "done" looks like. Task agents read
|
|
26
25
|
the deck's own docs for framework/API detail, so never restate recipes,
|
|
@@ -28,23 +27,22 @@ file layouts, or implementation steps.
|
|
|
28
27
|
\`\`\`
|
|
29
28
|
|
|
30
29
|
- Use \`after:\` only when a task truly builds on or would conflict with another (it may reference tasks spawned in this same reply, by title). Independent tasks must NOT wait on each other.
|
|
31
|
-
|
|
32
|
-
-
|
|
30
|
+
|
|
31
|
+
Keeping the board clean. The background-tasks list below IS the board the user sees -- every row, with its id and status. Be diligent about removing rows that no longer belong, using these two fences:
|
|
33
32
|
|
|
34
33
|
\`\`\`castle-done
|
|
35
|
-
comma-separated titles or ids
|
|
34
|
+
comma-separated finished-task titles or ids, or \`all\`
|
|
36
35
|
\`\`\`
|
|
37
|
-
|
|
38
|
-
- NEVER check a task off on your own judgment -- only a clear user statement that it works (or an explicit ask to clear it) counts. When in doubt, leave the row on the board.
|
|
39
|
-
- The background-tasks list below IS the board the user sees -- every row, with its id. To clear the WHOLE board at once (e.g. "clear all the tasks"), use \`all\` instead of listing ids: a \`castle-done\` with body \`all\` checks off every finished row, and a \`castle-stop\` with body \`all\` stops everything still running. Never claim the board is cleared without actually emitting the fence.
|
|
40
|
-
- To STOP tasks (running or waiting) when the user asks or their work is clearly no longer wanted, include:
|
|
41
|
-
|
|
42
36
|
\`\`\`castle-stop
|
|
43
|
-
comma-separated titles or ids
|
|
37
|
+
comma-separated active-task titles or ids, or \`all\`
|
|
44
38
|
\`\`\`
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
-
|
|
40
|
+
- \`castle-done\` removes FINISHED rows (done/failed). Use it when the user confirms a task works, or to clear a finished task that has become obsolete (replaced by newer work). \`all\` clears every finished row.
|
|
41
|
+
- \`castle-stop\` stops AND removes ACTIVE rows (running/waiting) -- a running task's agent is killed. Use it when the user asks to stop something, or when you spawn a fix/replacement that makes an in-flight task obsolete. \`all\` stops everything active.
|
|
42
|
+
- When you spawn a task that fixes, redoes, or replaces an earlier one, remove the earlier one in the SAME reply: \`castle-done\` if it already finished, \`castle-stop\` if it is still running or waiting. Keep the board meaning "what to look at right now".
|
|
43
|
+
- Do not mark a task done to imply YOU verified its quality -- the user playtests and confirms that. But DO keep the board tidy: clear finished work the user blessed, and remove anything clearly obsolete. When genuinely unsure, leave the row.
|
|
44
|
+
- Never claim the board is cleared without actually emitting the fence.
|
|
45
|
+
- Tasks are one-and-done -- when the user gives feedback on a finished task, spawn a new fix task (and \`castle-done\` the old row) rather than reopening it.
|
|
48
46
|
- Task agents are capable coding agents working in this same deck directory, but they know nothing about this conversation beyond your prompt.
|
|
49
47
|
|
|
50
48
|
Conversation style:
|
package/dist/agent.js
CHANGED
|
@@ -113,8 +113,7 @@ function visibleLength(raw) {
|
|
|
113
113
|
return raw.length;
|
|
114
114
|
}
|
|
115
115
|
// Pull ```castle-task fenced directives out of a finished router reply.
|
|
116
|
-
// Block format: title line, then optional "after:"
|
|
117
|
-
// either order), then the task prompt.
|
|
116
|
+
// Block format: title line, then an optional "after:" line, then the prompt.
|
|
118
117
|
function extractDirectives(full) {
|
|
119
118
|
const directives = [];
|
|
120
119
|
const checkoffs = [];
|
|
@@ -135,9 +134,9 @@ function extractDirectives(full) {
|
|
|
135
134
|
const cleaned = withoutDone.replace(fenceRe, (_match, body) => {
|
|
136
135
|
const lines = String(body).replace(/\r/g, '').split('\n');
|
|
137
136
|
const title = (lines.shift() ?? '').trim();
|
|
138
|
-
const headers = { after: []
|
|
137
|
+
const headers = { after: [] };
|
|
139
138
|
while (lines.length > 0) {
|
|
140
|
-
const headerMatch = /^(after
|
|
139
|
+
const headerMatch = /^(after):\s*(.*)$/i.exec((lines[0] ?? '').trim());
|
|
141
140
|
if (!headerMatch)
|
|
142
141
|
break;
|
|
143
142
|
lines.shift();
|
|
@@ -148,7 +147,7 @@ function extractDirectives(full) {
|
|
|
148
147
|
}
|
|
149
148
|
const prompt = lines.join('\n').trim();
|
|
150
149
|
if (title) {
|
|
151
|
-
directives.push({ title, after: headers.after,
|
|
150
|
+
directives.push({ title, after: headers.after, prompt });
|
|
152
151
|
}
|
|
153
152
|
return '';
|
|
154
153
|
});
|
|
@@ -528,6 +527,9 @@ function createTaskStore(opts) {
|
|
|
528
527
|
refreshTaskFiles(tasksDir, task);
|
|
529
528
|
const wasStopped = stopRequested.delete(task.id);
|
|
530
529
|
task.status = wasStopped ? 'interrupted' : result.ok ? 'done' : 'failed';
|
|
530
|
+
// A stopped task is cleared off the board (castle-stop = halt + remove).
|
|
531
|
+
if (wasStopped)
|
|
532
|
+
task.acknowledged = true;
|
|
531
533
|
if (result.ok && !wasStopped)
|
|
532
534
|
task.progress = 100;
|
|
533
535
|
task.finishedAt = nowIso();
|
|
@@ -543,18 +545,6 @@ function createTaskStore(opts) {
|
|
|
543
545
|
});
|
|
544
546
|
}
|
|
545
547
|
function spawnFromDirective(directive, originMessageId) {
|
|
546
|
-
// A fix/redo task sweeps the rows it obsoletes off the user's board. A
|
|
547
|
-
// superseded task that never started must not start later either.
|
|
548
|
-
for (const id of resolveDeps(tasks, directive.supersedes)) {
|
|
549
|
-
const old = tasks.get(id);
|
|
550
|
-
if (old && !old.acknowledged) {
|
|
551
|
-
if (old.status === 'waiting')
|
|
552
|
-
old.status = 'interrupted';
|
|
553
|
-
old.acknowledged = true;
|
|
554
|
-
touch(old);
|
|
555
|
-
opts.onSuperseded(old);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
548
|
const task = {
|
|
559
549
|
id: nanoid(8),
|
|
560
550
|
title: directive.title,
|
|
@@ -620,6 +610,30 @@ function createTaskStore(opts) {
|
|
|
620
610
|
// stop everything still active. Waiting tasks are cancelled outright;
|
|
621
611
|
// running ones get their agent process killed and finalize as interrupted
|
|
622
612
|
// via the stopRequested path.
|
|
613
|
+
// Halt + remove an active task (castle-stop): a waiting one is cancelled and
|
|
614
|
+
// cleared off the board immediately; a running one gets its agent process
|
|
615
|
+
// killed and is cleared when it finalizes (the stopRequested path acks it).
|
|
616
|
+
// No-op on terminal tasks.
|
|
617
|
+
function haltTask(task) {
|
|
618
|
+
if (task.status === 'waiting') {
|
|
619
|
+
task.status = 'interrupted';
|
|
620
|
+
task.acknowledged = true;
|
|
621
|
+
touch(task);
|
|
622
|
+
}
|
|
623
|
+
else if (task.status === 'running') {
|
|
624
|
+
stopRequested.add(task.id);
|
|
625
|
+
for (const child of children) {
|
|
626
|
+
if (child.pid === task.pid) {
|
|
627
|
+
try {
|
|
628
|
+
child.kill('SIGKILL');
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
/* already gone */
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
623
637
|
function stop(tokens) {
|
|
624
638
|
const ids = meansAll(tokens)
|
|
625
639
|
? [...tasks.values()]
|
|
@@ -628,25 +642,8 @@ function createTaskStore(opts) {
|
|
|
628
642
|
: resolveDeps(tasks, tokens);
|
|
629
643
|
for (const id of ids) {
|
|
630
644
|
const task = tasks.get(id);
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
if (task.status === 'waiting') {
|
|
634
|
-
task.status = 'interrupted';
|
|
635
|
-
touch(task);
|
|
636
|
-
}
|
|
637
|
-
else if (task.status === 'running') {
|
|
638
|
-
stopRequested.add(task.id);
|
|
639
|
-
for (const child of children) {
|
|
640
|
-
if (child.pid === task.pid) {
|
|
641
|
-
try {
|
|
642
|
-
child.kill('SIGKILL');
|
|
643
|
-
}
|
|
644
|
-
catch {
|
|
645
|
-
/* already gone */
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
645
|
+
if (task)
|
|
646
|
+
haltTask(task);
|
|
650
647
|
}
|
|
651
648
|
}
|
|
652
649
|
return { sorted, get: (id) => tasks.get(id), spawnFromDirective, acknowledge, checkOff, stop, shutdown };
|
|
@@ -779,10 +776,13 @@ function runRouterTurnIn(ctx, instruction) {
|
|
|
779
776
|
messages: ctx.log.messages
|
|
780
777
|
.filter((m) => m.role !== 'log' && m.id !== message.id && m.status !== 'streaming')
|
|
781
778
|
.map((m) => ({ role: m.role, text: m.text })),
|
|
782
|
-
// Only the live board
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
tasks: ctx.taskStore
|
|
779
|
+
// Only the live board -- match what the user sees. Hide tasks that are
|
|
780
|
+
// BOTH acknowledged AND finished; an active (running/waiting) task always
|
|
781
|
+
// shows even if somehow acked, so nothing can ever go invisible mid-work.
|
|
782
|
+
tasks: ctx.taskStore
|
|
783
|
+
.sorted()
|
|
784
|
+
.filter((t) => !(t.acknowledged && isTerminal(t.status)))
|
|
785
|
+
.map(asPromptTask),
|
|
786
786
|
instruction,
|
|
787
787
|
});
|
|
788
788
|
const backend = ctx.backend();
|
|
@@ -983,7 +983,6 @@ export function createAgentServer(opts) {
|
|
|
983
983
|
onStarted: () => undefined,
|
|
984
984
|
onRetry: (task, attempt) => addLog(`agent died, retrying (${attempt}/${MAX_TASK_ATTEMPTS}): ${task.title}`),
|
|
985
985
|
onFinished: (task) => taskFeeds.map.delete(task.id),
|
|
986
|
-
onSuperseded: () => undefined,
|
|
987
986
|
onFeed: (task, entry) => taskFeeds.push(task, entry),
|
|
988
987
|
});
|
|
989
988
|
// A new user message interrupts the in-flight router reply: its partial
|
package/dist/chat-client.js
CHANGED
|
@@ -227,7 +227,10 @@ function TaskRow(props) {
|
|
|
227
227
|
expanded && task.status !== 'running' ? (React.createElement("div", { className: "task-notes", dangerouslySetInnerHTML: renderMarkdown(notes) })) : null));
|
|
228
228
|
}
|
|
229
229
|
function TaskBoard(props) {
|
|
230
|
-
|
|
230
|
+
// Hide only tasks that are acknowledged AND finished. An active
|
|
231
|
+
// (running/waiting) task always shows, even if somehow acked -- a running
|
|
232
|
+
// task must never go invisible while it (and its dependents) are live.
|
|
233
|
+
const visible = props.tasks.filter((t) => !(t.acknowledged && TERMINAL_TASK_STATUSES.includes(t.status)));
|
|
231
234
|
if (visible.length === 0)
|
|
232
235
|
return null;
|
|
233
236
|
return (React.createElement("div", { id: "chat-strip" }, visible.map((task) => (React.createElement(TaskRow, { key: task.id, task: task, feed: props.feeds[task.id], onAck: props.onAck })))));
|