copilot-operator 0.1.42 → 0.1.43
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/src/api/operator.controller.js +131 -0
- package/dist/src/api/operator.service.js +335 -3
- package/dist/src/api/server.js +16 -0
- package/dist/src/cli.js +12 -0
- package/dist/src/config/schema.js +10 -0
- package/dist/src/orchestrator/taskRunner.js +109 -38
- package/dist/src/plan/brief.js +57 -20
- package/dist/src/plan/changes.js +217 -0
- package/dist/src/plan/schema.js +1 -1
- package/dist/src/plan/systemGuide.js +597 -12
- package/dist/src/session/compose.js +15 -1
- package/dist/src/session/model.js +1 -1
- package/dist/src/session/store.js +60 -0
- package/dist/src/vcs/taskVcs.js +110 -1
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/WmzBz4QN36pcz4PL_cfEW/_buildManifest.js +1 -0
- package/dist/web/_next/static/chunks/499-751e0be9d4f86e2d.js +1 -0
- package/dist/web/_next/static/chunks/549-2415dd26f4e53d07.js +1 -0
- package/dist/web/_next/static/chunks/699-9d031f8c145a0465.js +1 -0
- package/dist/web/_next/static/chunks/867-aea30f560bc18eaf.js +1 -0
- package/dist/web/_next/static/chunks/897-5a34109533c3da50.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-56134fbb8cc7554f.js +1 -0
- package/dist/web/_next/static/chunks/app/history/{page-47973539ceff81d6.js → page-0bb537407fed5944.js} +1 -1
- package/dist/web/_next/static/chunks/app/import/{page-999d6cd958989723.js → page-8ef75fc08638e75f.js} +1 -1
- package/dist/web/_next/static/chunks/app/layout-dc602b727480a653.js +1 -0
- package/dist/web/_next/static/chunks/app/page-d73a8ff300862cd8.js +1 -0
- package/dist/web/_next/static/chunks/app/{page-e557d58476ae6383.js → sessions/page-28376d2e4a9b4738.js} +1 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-afc235a9171535d8.js +1 -0
- package/dist/web/_next/static/css/{0e55721108c300f2.css → 72c85e4322c21223.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +25 -28
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +25 -28
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +25 -28
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +25 -28
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +25 -28
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +25 -28
- package/dist/web/presets.html +3 -3
- package/dist/web/presets.txt +25 -28
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +25 -28
- package/dist/web/sessions.html +16 -0
- package/dist/web/sessions.txt +26 -0
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +25 -28
- package/package.json +4 -2
- package/dist/web/_next/static/chunks/204-db82ca47ff211e21.js +0 -1
- package/dist/web/_next/static/chunks/499-39b2c00bda3656f0.js +0 -1
- package/dist/web/_next/static/chunks/549-348b0a1404407e5a.js +0 -1
- package/dist/web/_next/static/chunks/867-7d86a692260006f1.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-a25c40436fc312db.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-6ee612d25e78b246.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-872d8b9fe8eedbbf.js +0 -1
- package/dist/web/_next/static/lDmqS7pWU_B_Ac5idPvdS/_buildManifest.js +0 -1
- /package/dist/web/_next/static/{lDmqS7pWU_B_Ac5idPvdS → WmzBz4QN36pcz4PL_cfEW}/_ssgManifest.js +0 -0
|
@@ -39,7 +39,7 @@ import { confinementRefusal, sessionRoots } from '../exec/confinement.js';
|
|
|
39
39
|
import { writeReport } from '../exec/reportFile.js';
|
|
40
40
|
import { Pacer } from '../util/pacing.js';
|
|
41
41
|
import { RunLog } from '../log/runLog.js';
|
|
42
|
-
import { composeOpening, readOnlyNote } from '../session/compose.js';
|
|
42
|
+
import { composeOpening, operatorNotesSection, readOnlyNote } from '../session/compose.js';
|
|
43
43
|
import { MIN_TRIED_APPROACHES } from '../protocol/replySchema.js';
|
|
44
44
|
import { enforceScope, scopeMessage, scopeNote } from '../vcs/scope.js';
|
|
45
45
|
import { PLAN_FIRST, PLAN_NOT_NOW, PLAN_NUDGE_AFTER, describePlan, planApprovedMessage, planFrom, planModeNote, planNudge, planReviseMessage } from './planMode.js';
|
|
@@ -47,6 +47,7 @@ import { composeHandoff } from '../session/handoff.js';
|
|
|
47
47
|
import { ProgressWatch } from './progress.js';
|
|
48
48
|
import { contractConflicts, readsTreeClean } from './contract.js';
|
|
49
49
|
import { treeFingerprint } from '../vcs/git.js';
|
|
50
|
+
import { isContinuable } from '../session/model.js';
|
|
50
51
|
import { prepareForTask, commitTaskResult, commitShortfall, repoDirOf, trackedRepoOf, whereLeft } from '../vcs/taskVcs.js';
|
|
51
52
|
import { protectInputs, inputsMessage, inputsAtCommit, untrackedInputs, blobOfFile } from '../vcs/inputs.js';
|
|
52
53
|
import { artifactPatterns, artifactState, keepArtifacts } from '../vcs/artifacts.js';
|
|
@@ -1399,6 +1400,8 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1399
1400
|
// --- opening messages -------------------------------------------------------------
|
|
1400
1401
|
const { content: level1 } = await store.getLevel1();
|
|
1401
1402
|
const taskNumber = session.tasks.findIndex((t) => t.id === task.id) + 1;
|
|
1403
|
+
// The operator's notes not sent yet, marked sent as they go into this opening.
|
|
1404
|
+
const openingNotes = await store.takeUnsentNotes(session.id, task.id).catch(() => []);
|
|
1402
1405
|
const opening = composeOpening({
|
|
1403
1406
|
level1,
|
|
1404
1407
|
level2: task.level2,
|
|
@@ -1440,7 +1443,12 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1440
1443
|
...(planning && planMode !== 'off'
|
|
1441
1444
|
? { planNote: planModeNote(planMode, planEnforced, planMode === 'approve' && !!deps.approvePlan && (deps.currentMode?.() ?? cfg.execution.mode) === 'confirm') }
|
|
1442
1445
|
: {}),
|
|
1446
|
+
// The operator's notes about this task not sent yet: they go out with this opening (2026-10-14).
|
|
1447
|
+
...(openingNotes.length > 0 ? { operatorNotes: operatorNotesSection(openingNotes, false) } : {}),
|
|
1443
1448
|
});
|
|
1449
|
+
if (openingNotes.length > 0) {
|
|
1450
|
+
sink.event('operator-note-sent', { notes: openingNotes.length, with: 'opening' }, `${openingNotes.length} note(s) from the operator go out with the opening`);
|
|
1451
|
+
}
|
|
1444
1452
|
await setTask((t) => {
|
|
1445
1453
|
t.firstMessage = opening.firstMessage;
|
|
1446
1454
|
});
|
|
@@ -2657,6 +2665,15 @@ export async function runTask(transport, session, task, deps) {
|
|
|
2657
2665
|
if (planningRounds === PLAN_NUDGE_AFTER)
|
|
2658
2666
|
runnerNotes.push(planNudge(planningRounds));
|
|
2659
2667
|
}
|
|
2668
|
+
/*
|
|
2669
|
+
* What the operator wrote while the task works (2026-10-14: like Claude Code, the user can say more
|
|
2670
|
+
* while it runs): it goes out with this report, and is marked sent.
|
|
2671
|
+
*/
|
|
2672
|
+
const fromOperator = await store.takeUnsentNotes(session.id, task.id).catch(() => []);
|
|
2673
|
+
if (fromOperator.length > 0) {
|
|
2674
|
+
runnerNotes.push(operatorNotesSection(fromOperator, true));
|
|
2675
|
+
sink.event('operator-note-sent', { notes: fromOperator.length, with: 'report', iteration: iterations }, `${fromOperator.length} note(s) from the operator go out with this report`);
|
|
2676
|
+
}
|
|
2660
2677
|
await guardInputs(iterations);
|
|
2661
2678
|
// --- report back ---------------------------------------------------------------
|
|
2662
2679
|
const report = await writeReport(results, {
|
|
@@ -2926,52 +2943,106 @@ export async function runSession(sessionId, deps) {
|
|
|
2926
2943
|
*/
|
|
2927
2944
|
const retriesAllowed = cfg.limits.retryBlockedInFreshChat ?? 0;
|
|
2928
2945
|
let retried = 0;
|
|
2929
|
-
|
|
2930
|
-
|
|
2946
|
+
/*
|
|
2947
|
+
* Keep going until it is done (operator's request, 2026-10-14: "like Claude Code — the work does not stop
|
|
2948
|
+
* until the task is ready, and the limits keep it from looping for ever"). A task stopped by a limit from
|
|
2949
|
+
* the settings — messages, minutes, the reply wait, replies in the wrong shape, rounds of fixing checks or
|
|
2950
|
+
* review findings — is carried on at once in its own chat, from where it got to, as "Continue" would, up
|
|
2951
|
+
* to `limits.autoContinue` times; that count is the bound on the whole, so a task gets at most
|
|
2952
|
+
* (autoContinue + 1) × maxIterations messages and as many × maxRunMinutes. Not a task the operator
|
|
2953
|
+
* stopped, not one that blocked with nothing to do (that goes to a fresh chat below, then to a person),
|
|
2954
|
+
* and not when a pause or Stop was asked for.
|
|
2955
|
+
*/
|
|
2956
|
+
const autoAllowed = Math.max(0, cfg.limits.autoContinue ?? 0);
|
|
2957
|
+
let autoContinued = 0;
|
|
2958
|
+
const carriedOnAtOnce = async () => {
|
|
2959
|
+
if (autoContinued >= autoAllowed || deps.signal?.aborted || deps.shouldPause?.())
|
|
2960
|
+
return false;
|
|
2961
|
+
const now = (await store.getSession(sessionId))?.tasks.find((x) => x.id === task.id);
|
|
2962
|
+
if (!now || !now.limit || now.status === 'aborted' || !isContinuable(now))
|
|
2963
|
+
return false;
|
|
2964
|
+
autoContinued += 1;
|
|
2931
2965
|
bus.publish({
|
|
2932
2966
|
sessionId,
|
|
2933
2967
|
taskId: task.id,
|
|
2934
|
-
type: 'task-
|
|
2968
|
+
type: 'task-auto-continue',
|
|
2935
2969
|
level: 'warn',
|
|
2936
|
-
message: `"${task.title}"
|
|
2937
|
-
data: {
|
|
2970
|
+
message: `"${task.title}" stopped at ${now.limit.setting} (${now.limit.value}); carrying it on in its own chat, from where it got to (${autoContinued} of ${autoAllowed})`,
|
|
2971
|
+
data: { setting: now.limit.setting, value: now.limit.value, time: autoContinued, of: autoAllowed, status: now.status },
|
|
2938
2972
|
});
|
|
2939
|
-
await store.
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2973
|
+
await store.continueTask(sessionId, task.id);
|
|
2974
|
+
session = (await store.getSession(sessionId));
|
|
2975
|
+
const again = session.tasks.find((x) => x.id === task.id);
|
|
2976
|
+
if (!again)
|
|
2977
|
+
return false;
|
|
2978
|
+
outcome = await runTask(chat, session, again, deps);
|
|
2979
|
+
ran += 1;
|
|
2980
|
+
return true;
|
|
2981
|
+
};
|
|
2982
|
+
for (;;) {
|
|
2983
|
+
while (outcome.status === 'blocked' && retried < retriesAllowed && !deps.signal?.aborted && !(outcome.stopCode && NOT_THE_CHATS.includes(outcome.stopCode))
|
|
2984
|
+
// A task a limit stopped is carried on where it is, with its work, not started over in a fresh chat.
|
|
2985
|
+
&& !(autoContinued < autoAllowed && !!(await store.getSession(sessionId))?.tasks.find((x) => x.id === task.id)?.limit)) {
|
|
2986
|
+
retried += 1;
|
|
2987
|
+
bus.publish({
|
|
2988
|
+
sessionId,
|
|
2989
|
+
taskId: task.id,
|
|
2990
|
+
type: 'task-retry-fresh-chat',
|
|
2991
|
+
level: 'warn',
|
|
2992
|
+
message: `"${task.title}" ended blocked; running it again in a fresh conversation (${retried} of ${retriesAllowed})`,
|
|
2993
|
+
data: { attempt: retried, of: retriesAllowed, reason: outcome.reason },
|
|
2994
|
+
});
|
|
2995
|
+
await store.rerunTask(sessionId, task.id, {});
|
|
2996
|
+
// The conversation pointer is dropped, so the next message opens a new chat, registers
|
|
2997
|
+
// it and sends the contract again. The old chat stays where it is, readable.
|
|
2964
2998
|
session = await store.updateSession(sessionId, (s) => {
|
|
2965
|
-
s.
|
|
2999
|
+
s.chat = undefined;
|
|
3000
|
+
s.contractSent = false;
|
|
3001
|
+
// Marked, not counted: how many retries this run made is read back from the marks
|
|
3002
|
+
// (`freshRetriesOfLatestRun`), so a later run cannot add to an earlier one's count.
|
|
3003
|
+
const again = s.tasks.find((x) => x.id === task.id);
|
|
3004
|
+
if (again)
|
|
3005
|
+
again.freshRetry = true;
|
|
2966
3006
|
});
|
|
3007
|
+
await chat.newChat();
|
|
3008
|
+
/*
|
|
3009
|
+
* The picker belongs to the conversation, so a new conversation has lost the choice.
|
|
3010
|
+
*
|
|
3011
|
+
* The model is applied once, before the first task, because that is where the
|
|
3012
|
+
* conversation is opened — and this is the other place one is opened, so it has to be
|
|
3013
|
+
* applied again here. Without it a session that asked for a particular model ran its
|
|
3014
|
+
* retries on whatever the chat opens on, which is "Auto", and the record went on saying
|
|
3015
|
+
* it was the model the session asked for. The review has always done this for its own
|
|
3016
|
+
* fresh conversations a few hundred lines up; this path simply never did.
|
|
3017
|
+
*/
|
|
3018
|
+
const freshModel = await applySessionModel(chat, session, bus, cfg, store, deps.models);
|
|
3019
|
+
if (effectiveModels(session, cfg).model) {
|
|
3020
|
+
session = await store.updateSession(sessionId, (s) => {
|
|
3021
|
+
s.modelInUse = freshModel.current;
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
if (freshModel.refused)
|
|
3025
|
+
throw new Error(freshModel.refused);
|
|
3026
|
+
const fresh = session.tasks.find((x) => x.id === task.id);
|
|
3027
|
+
if (!fresh)
|
|
3028
|
+
break;
|
|
3029
|
+
outcome = await runTask(chat, session, fresh, deps);
|
|
3030
|
+
ran += 1;
|
|
2967
3031
|
}
|
|
2968
|
-
if (
|
|
2969
|
-
throw new Error(freshModel.refused);
|
|
2970
|
-
const fresh = session.tasks.find((x) => x.id === task.id);
|
|
2971
|
-
if (!fresh)
|
|
3032
|
+
if (!(await carriedOnAtOnce()))
|
|
2972
3033
|
break;
|
|
2973
|
-
|
|
2974
|
-
|
|
3034
|
+
}
|
|
3035
|
+
if (autoContinued > 0) {
|
|
3036
|
+
bus.publish({
|
|
3037
|
+
sessionId,
|
|
3038
|
+
taskId: task.id,
|
|
3039
|
+
type: 'task-auto-continue-ended',
|
|
3040
|
+
level: outcome.status === 'done' ? 'info' : 'warn',
|
|
3041
|
+
message: outcome.status === 'done'
|
|
3042
|
+
? `"${task.title}" done after being carried on ${autoContinued} time(s) on its own`
|
|
3043
|
+
: `"${task.title}" ended ${outcome.status} after being carried on ${autoContinued} time(s) on its own${autoContinued >= autoAllowed ? ` — limits.autoContinue (${autoAllowed}) reached` : ''}`,
|
|
3044
|
+
data: { times: autoContinued, of: autoAllowed, status: outcome.status },
|
|
3045
|
+
});
|
|
2975
3046
|
}
|
|
2976
3047
|
if (retried > 0) {
|
|
2977
3048
|
bus.publish({
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -796,7 +796,16 @@ Skipped entirely when every task ended **"done"**: say so in one line and go to
|
|
|
796
796
|
|
|
797
797
|
Otherwise: diagnose first, propose second, and say which kind of proposal it is.
|
|
798
798
|
|
|
799
|
-
**
|
|
799
|
+
**The quickest way to what happened is the operator's own report.** On \`/\`, the workbench, they choose
|
|
800
|
+
the task or its session, press **"Ask Kerrigan"**, write what they saw and want, and press **"Copy for
|
|
801
|
+
Kerrigan"**: you get every task with its id, how it ended and why, what it changed, and their words,
|
|
802
|
+
with the form of the answer. Answer it with **changes** to those sessions — a task carried on with your
|
|
803
|
+
prompt (\`continue\`), a done one given a new prompt (\`newPrompt\`), one started over (\`runAgain\`),
|
|
804
|
+
undone (\`undo\`), accepted as done (\`accept\`), a task added, edited or removed — not with a new plan,
|
|
805
|
+
which would make copies in new chats. They paste your whole answer under **"Her answer"**, press **"Check
|
|
806
|
+
her answer"**, read each change in words, and **"Make the {n} change(s)"**; nothing starts by itself.
|
|
807
|
+
|
|
808
|
+
**When you need more than that, ask for the exports**, with the record message from the script. Every row on \`/history\` carries
|
|
800
809
|
three links, each for that one task:
|
|
801
810
|
|
|
802
811
|
- **"plan"** — the sessions and tasks as they are now, in this format, with the edits made in the
|
|
@@ -812,7 +821,7 @@ three links, each for that one task:
|
|
|
812
821
|
**"save the log"** on the same row writes the runner's log of the latest attempt onto the
|
|
813
822
|
operator's Desktop and opens Explorer with the file selected, ready to drag into this chat;
|
|
814
823
|
**"save this attempt's log"** does the same for an earlier one. **"What happened"** unfolds the
|
|
815
|
-
whole story under the row without saving anything. For a whole run rather than one task, it is **"Download everything as JSON"** on
|
|
824
|
+
whole story under the row without saving anything. For a whole run rather than one task, it is **"Download everything as JSON"** on \`/sessions\`:
|
|
816
825
|
one file with every step, exit code, summary, review and commit of those sessions.
|
|
817
826
|
|
|
818
827
|
**Read \`whyItFailed\` first**, then the last rounds. The failure words: \`blocked\` — the chat
|
|
@@ -830,7 +839,11 @@ in those words, every time:
|
|
|
830
839
|
|
|
831
840
|
- **A new prompt.** The task text was ambiguous, a path or a port was wrong, the level 2 forbade
|
|
832
841
|
what the task needed, a check asked the git index about a file the runner had not committed yet,
|
|
833
|
-
or the chat did the wrong thing and the checks caught it.
|
|
842
|
+
or the chat did the wrong thing and the checks caught it. **Carry the task on with it first**: it keeps
|
|
843
|
+
what the failed attempt did, in its own chat and branch, and the chat is told how it ended and why.
|
|
844
|
+
Give the text, as a \`continue\` change, or for the operator to write on \`/\`: choose the task, put
|
|
845
|
+
the text in the chat box on the right, press **"Continue with this"**, then start it from the panel that
|
|
846
|
+
opens. Only when the earlier work should be thrown away, give the whole replacement text, then:
|
|
834
847
|
1. On \`/history\`, in the **"Flow"** view, press **"Fix the prompt and queue it again"** on that row.
|
|
835
848
|
2. Replace the text with the one above and press **"Save and queue again"**.
|
|
836
849
|
3. Under **"What is next"**, press **"Continue: run the {n} queued task(s) in {s} session(s)"**
|
|
@@ -850,7 +863,10 @@ in those words, every time:
|
|
|
850
863
|
When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
|
|
851
864
|
while you work it out.
|
|
852
865
|
Putting the repository back to before the task is **"Restore"**; putting it back and re-running
|
|
853
|
-
that task and every task after it is **"Run again from here"**.
|
|
866
|
+
that task and every task after it is **"Run again from here"**. On \`/\`, **"Undo…"** on the task offers
|
|
867
|
+
**"Undo only this task"** — its work taken out of the session's branch, what came after it kept — and
|
|
868
|
+
**"Go back to before this task"** — it and the tasks after it queued again, nothing started. A task the
|
|
869
|
+
operator finished by hand is **"Accept as done"**.
|
|
854
870
|
- **A change to the bot.** No wording fixes it: the shell the task needs is not on the machine,
|
|
855
871
|
the claim cannot be written as any of the \`expect\` kinds in the checks table above, the runner
|
|
856
872
|
never picked up what the chat attached, the review did not run at all. Say **this is a change
|
|
@@ -936,17 +952,20 @@ answer means, and what you do on it:
|
|
|
936
952
|
for a criterion that is only **claimed**, because it is the cheapest: it changes nothing and turns
|
|
937
953
|
"claimed only" into "proven" or into a real failure you can then fix. Write it as a plan in this
|
|
938
954
|
format.
|
|
939
|
-
2. **
|
|
940
|
-
asked wrongly.
|
|
955
|
+
2. **Give a done task a new prompt** — when the work is right as far as it goes but more is needed,
|
|
956
|
+
or it was asked wrongly. A \`newPrompt\` change, or on \`/\`: the task, its chat box, **"Queue with
|
|
957
|
+
this new prompt"** — it builds on what the task did. To start it over instead: on \`/history\`,
|
|
941
958
|
**"Fix the prompt and queue it again"** on that row, replace the text, **"Save and queue
|
|
942
959
|
again"**, then continue the run.
|
|
943
960
|
3. **Put the repository back and solve it differently** — when the approach is wrong rather than
|
|
944
|
-
the wording, and building on it would be building on the wrong thing.
|
|
961
|
+
the wording, and building on it would be building on the wrong thing. An \`undo\` change, or on
|
|
962
|
+
\`/\` **"Undo…"**: **"Undo only this task"** takes its work out and keeps what came after;
|
|
963
|
+
**"Go back to before this task"** queues it and every one after it again. **"Restore"** on the
|
|
945
964
|
task card puts the code back to before that task; **"Run again from here"** puts it back and
|
|
946
965
|
re-runs that task and every one after it. Say what will be lost.
|
|
947
|
-
4. **
|
|
948
|
-
|
|
949
|
-
**"Create the sessions and tasks"**.
|
|
966
|
+
4. **More tasks** — when what is missing was never asked for by any task, so there is nothing to
|
|
967
|
+
fix and something to add. In the same session, as \`addTask\` changes; as a new session, a plan
|
|
968
|
+
in this format, for **"Create the sessions and tasks"**.
|
|
950
969
|
`.trim();
|
|
951
970
|
const PHASE3_BG = `
|
|
952
971
|
## Фаза 3 — задача не завърши готова
|
|
@@ -956,7 +975,16 @@ const PHASE3_BG = `
|
|
|
956
975
|
|
|
957
976
|
Иначе: първо диагноза, после предложение, и всеки път казвай от кой вид е предложението.
|
|
958
977
|
|
|
959
|
-
|
|
978
|
+
**Най-бързият път до случилото се е отчетът на самия оператор.** В \`/\`, работното място, той избира
|
|
979
|
+
задачата или сесията ѝ, натиска **„Попитай Кериган“**, пише какво е видял и какво иска и натиска
|
|
980
|
+
**„Копирай за Кериган“**: получаваш всяка задача с id-то ѝ, как е завършила и защо, какво е променила, и
|
|
981
|
+
думите му, с формата на отговора. Отговори с **промени** по тези сесии — задача, продължена с твоя prompt
|
|
982
|
+
(\`continue\`), готова с нов prompt (\`newPrompt\`), пусната отначало (\`runAgain\`), отменена (\`undo\`),
|
|
983
|
+
приета като готова (\`accept\`), добавена, редактирана или премахната задача — а не с нов план, който би
|
|
984
|
+
направил копия в нови чатове. Той поставя целия ти отговор под **„Нейният отговор“**, натиска **„Провери
|
|
985
|
+
отговора ѝ“**, чете всяка промяна с думи и **„Направи промените ({n})“**; нищо не тръгва само.
|
|
986
|
+
|
|
987
|
+
**Когато ти трябва повече, поискай файловете**, със съобщението за записа от сценария. Всеки ред в \`/history\` носи три
|
|
960
988
|
връзки, всяка за точно тази задача:
|
|
961
989
|
|
|
962
990
|
- **„план“** — сесиите и задачите, както са сега, в този формат, с редакциите от интерфейса.
|
|
@@ -973,7 +1001,7 @@ const PHASE3_BG = `
|
|
|
973
1001
|
оператора и отваря Explorer с избран файл, готов за влачене в този чат; **„запази log-а на този
|
|
974
1002
|
опит“** прави същото за по-ранен. **„Какво се случи“** разгъва цялата история под реда, без да се
|
|
975
1003
|
записва нищо. За цяло пускане, а не
|
|
976
|
-
за една задача, е **„Изтегли всичко като JSON“** в
|
|
1004
|
+
за една задача, е **„Изтегли всичко като JSON“** в \`/sessions\`: един файл с всяка стъпка, код на изход,
|
|
977
1005
|
обяснение, рецензия и комит на тези сесии.
|
|
978
1006
|
|
|
979
1007
|
**Чети първо \`whyItFailed\`**, после последните кръгове. Думите за провал: \`blocked\` — чатът
|
|
@@ -992,7 +1020,10 @@ const PHASE3_BG = `
|
|
|
992
1020
|
- **Нов prompt.** Текстът на задачата е бил двусмислен, път или порт е грешен, ниво 2 е
|
|
993
1021
|
забранявало това, което задачата иска, проверка е питала git индекса за файл, който runner-ът
|
|
994
1022
|
още не е комитнал, или чатът е направил грешното нещо и проверките са го хванали. Дай целия
|
|
995
|
-
|
|
1023
|
+
текст. **Първо продължи задачата с него**: пази каквото е свършил провалилият се опит, в своя чат и
|
|
1024
|
+
клон, а чатът научава как е завършил и защо. Дай текста като промяна \`continue\` или за оператора в
|
|
1025
|
+
\`/\`: задачата, текстът в полето на чата вдясно, **„Продължи с това“**, после пускане от панела,
|
|
1026
|
+
който се отваря. Само когато по-ранната работа трябва да се изхвърли, дай целия нов текст, после:
|
|
996
1027
|
1. В \`/history\`, в изгледа **„Поток“**, натисни **„Поправи prompt-а и върни в опашката“** на този ред.
|
|
997
1028
|
2. Замени текста с горния и натисни **„Запази и върни в опашката“**.
|
|
998
1029
|
3. Под **„Какво следва“** натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“** (пише
|
|
@@ -1012,7 +1043,10 @@ const PHASE3_BG = `
|
|
|
1012
1043
|
Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
|
|
1013
1044
|
останалото, докато го разбереш.
|
|
1014
1045
|
Връщането на хранилището отпреди задачата е **„Върни“**; връщането му плюс ново изпълнение на
|
|
1015
|
-
тази задача и всички след нея е **„Пусни отново оттук“**.
|
|
1046
|
+
тази задача и всички след нея е **„Пусни отново оттук“**. В \`/\` **„Отмени…“** на задачата предлага
|
|
1047
|
+
**„Отмени само тази задача“** — работата ѝ се изважда от клона на сесията, каквото е след нея остава — и
|
|
1048
|
+
**„Върни отпреди тази задача“** — тя и задачите след нея отново на опашката, без пускане. Задача,
|
|
1049
|
+
довършена ръчно от оператора, е **„Приеми като готова“**.
|
|
1016
1050
|
- **Промяна по бота.** Никакви думи не го оправят: обвивката, която задачата иска, я няма на
|
|
1017
1051
|
машината, твърдението не се изразява с нито един от видовете \`expect\` от таблицата с
|
|
1018
1052
|
проверките по-горе, runner-ът изобщо не е взел това, което чатът е прикачил, рецензията не се
|
|
@@ -1099,17 +1133,20 @@ const PHASE4_BG = `
|
|
|
1099
1133
|
я първа за критерий, който е само **твърдение**, защото е най-евтината: не променя нищо и превръща
|
|
1100
1134
|
„само твърдение“ в „доказано“ или в истински провал, който после можеш да поправиш. Напиши я като
|
|
1101
1135
|
план в този формат.
|
|
1102
|
-
2.
|
|
1103
|
-
|
|
1136
|
+
2. **Дай на готова задача нов prompt** — когато работата е правилна дотук, но трябва още, или е била
|
|
1137
|
+
поискана грешно. Промяна \`newPrompt\` или в \`/\`: задачата, полето на чата, **„На опашката с този
|
|
1138
|
+
нов prompt“** — надгражда свършеното. За да тръгне отначало: в \`/history\`
|
|
1104
1139
|
**„Поправи prompt-а и върни в опашката“** на този ред, замени текста,
|
|
1105
1140
|
**„Запази и върни в опашката“**, после продължи изпълнението.
|
|
1106
1141
|
3. **Върни хранилището и реши иначе** — когато грешен е подходът, а не формулировката, и да се
|
|
1107
|
-
стъпва върху него значи да се стъпва върху грешното нещо.
|
|
1142
|
+
стъпва върху него значи да се стъпва върху грешното нещо. Промяна \`undo\` или в \`/\` **„Отмени…“**:
|
|
1143
|
+
**„Отмени само тази задача“** изважда работата ѝ и пази каквото е след нея; **„Върни отпреди тази
|
|
1144
|
+
задача“** слага нея и всяка след нея отново на опашката. **„Върни“** на картата на задачата
|
|
1108
1145
|
връща кода отпреди тази задача; **„Пусни отново оттук“** го връща и пуска пак нея и всяка след
|
|
1109
1146
|
нея. Кажи какво ще се загуби.
|
|
1110
|
-
4.
|
|
1111
|
-
|
|
1112
|
-
**„Създай сесиите и задачите“**.
|
|
1147
|
+
4. **Още задачи** — когато липсващото никога не е било поискано от никоя задача, така че няма какво
|
|
1148
|
+
да се поправя, а има какво да се добави. В същата сесия — като промени \`addTask\`; като нова сесия —
|
|
1149
|
+
план в този формат, за **„Създай сесиите и задачите“**.
|
|
1113
1150
|
`.trim();
|
|
1114
1151
|
/**
|
|
1115
1152
|
* The operator's own part, verbatim — or, while there is none, the interview that produces it.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kerrigan's answer about work that already exists (operator's request, 2026-10-14: "no dead end — the user
|
|
3
|
+
* tells Kerrigan what happened and she proposes how to go on; even when the tasks are done she may judge the
|
|
4
|
+
* implementation wrong and ask for more"). A plan only ever made new sessions, so everything she advised about
|
|
5
|
+
* a session that had run had to be done by hand, or became a copy of it in a new chat.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, both plain text so they work with any chat: `kerriganReport` writes what happened — each session
|
|
8
|
+
* and task with its id, how it ended and why, what it changed — together with the operator's note and the exact
|
|
9
|
+
* form of the answer; `checkChanges` reads her answer, a list of changes to those sessions and tasks, against
|
|
10
|
+
* them as they are now, and says in words what each would do before anything is done. The service applies them
|
|
11
|
+
* one by one through the same paths the buttons use, so a change is held to the same rules as a click.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { TaskInput, extractJson } from './schema.js';
|
|
15
|
+
import { canFollowUp, clipText, isContinuable } from '../session/model.js';
|
|
16
|
+
const SessionRef = z.string({ error: 'Each change names its "session": the id from the report (s-…), or its exact name.' }).trim().min(1);
|
|
17
|
+
const TaskRef = z.union([z.string().trim().min(1), z.number().int().positive()], {
|
|
18
|
+
error: 'Each change names its "task": the id from the report (t-…), its number in the session, or its exact title.',
|
|
19
|
+
});
|
|
20
|
+
export const ChangeSchema = z.discriminatedUnion('op', [
|
|
21
|
+
z.object({ op: z.literal('addTask'), session: SessionRef, after: TaskRef.optional(), task: TaskInput }),
|
|
22
|
+
z.object({
|
|
23
|
+
op: z.literal('editTask'),
|
|
24
|
+
session: SessionRef,
|
|
25
|
+
task: TaskRef,
|
|
26
|
+
title: z.string().trim().min(3).max(120).optional(),
|
|
27
|
+
prompt: z.string().trim().min(30, 'A new prompt needs a real instruction, not a few words.').optional(),
|
|
28
|
+
checks: TaskInput.shape.checks.optional(),
|
|
29
|
+
scope: TaskInput.shape.scope.optional(),
|
|
30
|
+
}),
|
|
31
|
+
z.object({ op: z.literal('removeTask'), session: SessionRef, task: TaskRef }),
|
|
32
|
+
z.object({ op: z.literal('continue'), session: SessionRef, task: TaskRef, instruction: z.string().trim().min(10, 'Say what to do now: "instruction" is the new prompt the task carries on with.') }),
|
|
33
|
+
z.object({ op: z.literal('newPrompt'), session: SessionRef, task: TaskRef, prompt: z.string().trim().min(30, 'A new prompt needs a real instruction, not a few words.') }),
|
|
34
|
+
z.object({ op: z.literal('runAgain'), session: SessionRef, task: TaskRef, prompt: z.string().trim().min(30).optional() }),
|
|
35
|
+
z.object({ op: z.literal('undo'), session: SessionRef, task: TaskRef, how: z.enum(['this', 'back'], { error: '"how" is "this" (take only this task\'s work out) or "back" (go back to before it, and queue the tasks after it too).' }) }),
|
|
36
|
+
z.object({ op: z.literal('accept'), session: SessionRef, task: TaskRef, note: z.string().trim().optional() }),
|
|
37
|
+
z.object({ op: z.literal('note'), session: SessionRef, task: TaskRef, text: z.string().trim().min(1) }),
|
|
38
|
+
], {
|
|
39
|
+
error: (i) => (i.code === 'invalid_union' ? '"op" must be one of addTask, editTask, removeTask, continue, newPrompt, runAgain, undo, accept, note.' : undefined),
|
|
40
|
+
});
|
|
41
|
+
export const ChangesSchema = z.object({
|
|
42
|
+
version: z.literal(1, 'Set "version": 1.'),
|
|
43
|
+
notes: z.string().trim().default(''),
|
|
44
|
+
changes: z.array(ChangeSchema).min(1, 'An answer with "changes" needs at least one.'),
|
|
45
|
+
});
|
|
46
|
+
/** Whether the pasted text is an answer with changes rather than a plan of new sessions. */
|
|
47
|
+
export function looksLikeChanges(text) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(extractJson(text ?? ''));
|
|
50
|
+
return !!raw && typeof raw === 'object' && Array.isArray(raw.changes);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const RUNNING = ['running', 'waiting-approval'];
|
|
57
|
+
function findSession(sessions, ref) {
|
|
58
|
+
return sessions.find((s) => s.id === ref) ?? sessions.find((s) => s.name === ref) ?? sessions.find((s) => s.name.toLowerCase() === ref.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
function findTask(session, ref) {
|
|
61
|
+
if (typeof ref === 'number' || /^\d+$/.test(ref))
|
|
62
|
+
return session.tasks[Number(ref) - 1];
|
|
63
|
+
return session.tasks.find((t) => t.id === ref) ?? session.tasks.find((t) => t.title === ref) ?? session.tasks.find((t) => t.title.toLowerCase() === ref.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
/** Reads Kerrigan's answer and finds each change against the sessions as they are now. Nothing is changed. */
|
|
66
|
+
export function checkChanges(text, sessions) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = JSON.parse(extractJson(text ?? ''));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { ok: false, warnings: [], issues: [{ path: '', message: `This is not valid JSON: ${e.message}.` }] };
|
|
73
|
+
}
|
|
74
|
+
const parsed = ChangesSchema.safeParse(raw);
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
return { ok: false, warnings: [], issues: parsed.error.issues.map((i) => ({ path: i.path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${String(p)}`)).join('').replace(/^\./, ''), message: i.message })) };
|
|
77
|
+
}
|
|
78
|
+
const issues = [];
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const changes = [];
|
|
81
|
+
parsed.data.changes.forEach((change, index) => {
|
|
82
|
+
const path = `changes[${index}]`;
|
|
83
|
+
const session = findSession(sessions, change.session);
|
|
84
|
+
if (!session) {
|
|
85
|
+
issues.push({ path: `${path}.session`, message: `No session "${change.session}". Use the id from the report (s-…) or the session's exact name.` });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const base = { index, change, sessionId: session.id, sessionName: session.name };
|
|
89
|
+
if (change.op === 'addTask') {
|
|
90
|
+
const after = change.after !== undefined ? findTask(session, change.after) : undefined;
|
|
91
|
+
if (change.after !== undefined && !after) {
|
|
92
|
+
issues.push({ path: `${path}.after`, message: `Session "${session.name}" has no task "${change.after}" to put the new one after.` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
changes.push({ ...base, says: `add task "${change.task.title}" to "${session.name}"${after ? `, after "${after.title}"` : ', at the end'}, queued` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const task = findTask(session, change.task);
|
|
99
|
+
if (!task) {
|
|
100
|
+
issues.push({ path: `${path}.task`, message: `Session "${session.name}" has no task "${change.task}". Use the id from the report (t-…), its number, or its exact title.` });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const named = { ...base, taskId: task.id, taskTitle: task.title };
|
|
104
|
+
const refuse = (message) => {
|
|
105
|
+
issues.push({ path: `${path}.op`, message: `"${task.title}" in "${session.name}" is ${task.status}: ${message}` });
|
|
106
|
+
};
|
|
107
|
+
if (RUNNING.includes(task.status))
|
|
108
|
+
return refuse('it is running now; nothing can be changed on it until it stops.');
|
|
109
|
+
switch (change.op) {
|
|
110
|
+
case 'editTask':
|
|
111
|
+
if (task.status !== 'queued')
|
|
112
|
+
return refuse('only a queued task is edited; for one that ran use "runAgain" with a "prompt", "continue" or "newPrompt".');
|
|
113
|
+
changes.push({ ...named, says: `edit "${task.title}": ${['title', 'prompt', 'checks', 'scope'].filter((k) => change[k] !== undefined).join(', ') || 'nothing'}` });
|
|
114
|
+
return;
|
|
115
|
+
case 'removeTask':
|
|
116
|
+
if (task.status !== 'queued')
|
|
117
|
+
return refuse('only a queued task is removed; a task that ran keeps its record.');
|
|
118
|
+
changes.push({ ...named, says: `remove the queued task "${task.title}"` });
|
|
119
|
+
return;
|
|
120
|
+
case 'continue':
|
|
121
|
+
if (!isContinuable(task) && !canFollowUp(task))
|
|
122
|
+
return refuse('only a task that failed, blocked or stopped is carried on; for a done one use "newPrompt".');
|
|
123
|
+
changes.push({ ...named, says: `carry "${task.title}" on in its own chat and branch, from where it got to, with the new prompt` });
|
|
124
|
+
return;
|
|
125
|
+
case 'newPrompt':
|
|
126
|
+
if (task.status !== 'done')
|
|
127
|
+
return refuse('"newPrompt" builds on a done task\'s work; for a failed one use "continue".');
|
|
128
|
+
changes.push({ ...named, says: `give the done task "${task.title}" a new prompt that builds on its work, queued` });
|
|
129
|
+
return;
|
|
130
|
+
case 'runAgain':
|
|
131
|
+
if (task.status === 'queued')
|
|
132
|
+
return refuse('it is queued already; use "editTask" to change it.');
|
|
133
|
+
changes.push({ ...named, says: `run "${task.title}" again from where it first started${change.prompt ? ', with a new prompt' : ''}, queued` });
|
|
134
|
+
return;
|
|
135
|
+
case 'undo':
|
|
136
|
+
if (task.status === 'queued' || !task.startedAt)
|
|
137
|
+
return refuse('it has not run since it was queued; there is nothing of it to undo.');
|
|
138
|
+
changes.push({ ...named, says: change.how === 'this' ? `undo only "${task.title}": its work taken out, the tasks after it kept; it is queued again` : `go back to before "${task.title}": it and the tasks after it queued again` });
|
|
139
|
+
return;
|
|
140
|
+
case 'accept':
|
|
141
|
+
if (!['failed', 'blocked', 'limit-reached', 'aborted'].includes(task.status))
|
|
142
|
+
return refuse('only a task that ended without being done is accepted as done.');
|
|
143
|
+
changes.push({ ...named, says: `accept "${task.title}" as done${change.note ? ', with a note' : ''}` });
|
|
144
|
+
return;
|
|
145
|
+
case 'note':
|
|
146
|
+
if (task.status !== 'queued')
|
|
147
|
+
warnings.push(`The note for "${task.title}" (${task.status}) goes out only when it runs again.`);
|
|
148
|
+
changes.push({ ...named, says: `a note to the chat for "${task.title}"` });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (issues.length > 0)
|
|
153
|
+
return { ok: false, issues, warnings };
|
|
154
|
+
return { ok: true, notes: parsed.data.notes, changes, warnings };
|
|
155
|
+
}
|
|
156
|
+
// --- the report ------------------------------------------------------------------------------
|
|
157
|
+
const clip = (s, n) => (clipText(s, n) ?? '').replace(/\s+/g, ' ').trim();
|
|
158
|
+
function taskLines(task, index) {
|
|
159
|
+
const out = [`${index + 1}. "${task.title}" — id ${task.id} — ${task.status}${(task.attempt ?? 1) > 1 ? `, attempt ${task.attempt}` : ''}`];
|
|
160
|
+
if (task.accepted)
|
|
161
|
+
out.push(` accepted as done by the operator (it had ended ${task.accepted.was})${task.accepted.note ? `: ${clip(task.accepted.note, 300)}` : ''}`);
|
|
162
|
+
const undone = task.attempts?.at(-1)?.undone;
|
|
163
|
+
if (task.status === 'queued' && undone)
|
|
164
|
+
out.push(` its last attempt was undone by the operator (${undone.how === 'this' ? 'only its work taken out' : 'back to before it'})`);
|
|
165
|
+
if (task.continuing?.instruction)
|
|
166
|
+
out.push(` carried on with the operator's prompt: ${clip(task.continuing.instruction, 400)}`);
|
|
167
|
+
if (task.status === 'queued')
|
|
168
|
+
out.push(` prompt: ${clip(task.prompt, 500)}`);
|
|
169
|
+
if (task.summary && task.status !== 'queued')
|
|
170
|
+
out.push(` summary: ${clip(task.summary, 600)}`);
|
|
171
|
+
if (task.reason && task.reason !== task.summary && task.status !== 'done')
|
|
172
|
+
out.push(` why it stopped: ${clip(task.reason, 600)}`);
|
|
173
|
+
const failed = (task.checkResults ?? []).filter((c) => !c.passed);
|
|
174
|
+
if (failed.length > 0)
|
|
175
|
+
out.push(` checks that did not pass: ${failed.slice(0, 6).map((c) => `${c.name} (${clip(c.detail, 160)})`).join('; ')}`);
|
|
176
|
+
if (task.review?.verdict) {
|
|
177
|
+
const findings = (task.review.findings ?? []).slice(0, 5).map((f) => `${clip(f.what, 200)}${f.where ? ` [${clip(f.where, 80)}]` : ''}`);
|
|
178
|
+
out.push(` review: ${task.review.verdict}${findings.length > 0 ? ` — ${findings.join('; ')}` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
const files = (task.vcs?.files ?? []).map((f) => f.path);
|
|
181
|
+
if (files.length > 0)
|
|
182
|
+
out.push(` changed: ${files.slice(0, 25).join(', ')}${files.length > 25 ? ` and ${files.length - 25} more` : ''}`);
|
|
183
|
+
if (task.vcs?.branch)
|
|
184
|
+
out.push(` branch ${task.vcs.branch}${task.vcs.commit ? ` at ${task.vcs.commit.slice(0, 8)}` : ''}`);
|
|
185
|
+
const notes = task.notes ?? [];
|
|
186
|
+
if (notes.length > 0)
|
|
187
|
+
out.push(` operator's notes: ${notes.slice(-3).map((n) => clip(n.text, 200)).join(' | ')}`);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* What happened, for Kerrigan: the sessions, each task with its id, how it ended and why, and what it changed;
|
|
192
|
+
* the operator's note; and the exact form of the answer. Plain text, for any chat.
|
|
193
|
+
*/
|
|
194
|
+
export function kerriganReport(sessions, opts = {}) {
|
|
195
|
+
const only = new Set(opts.taskIds ?? []);
|
|
196
|
+
const lines = [
|
|
197
|
+
'# What happened — for Kerrigan',
|
|
198
|
+
'',
|
|
199
|
+
`The operator sends you the state of their work as copilot-operator recorded it (${opts.at ?? new Date().toISOString()}), and asks you to look at it`,
|
|
200
|
+
'and propose how to go on — whether the tasks failed, stopped, or are done but not what was meant.',
|
|
201
|
+
'',
|
|
202
|
+
];
|
|
203
|
+
if (opts.note?.trim())
|
|
204
|
+
lines.push('## The operator says', '', opts.note.trim(), '');
|
|
205
|
+
for (const s of sessions) {
|
|
206
|
+
const vcs = s.vcs?.enabled ? `${s.vcs.branchMode === 'per-task' ? 'a branch per task' : 'one branch for the session, a commit per task'} in ${s.vcs.repoDir || '(no folder)'}` : 'no version control';
|
|
207
|
+
lines.push(`## Session "${s.name}" — id ${s.id}`, '', `${vcs}; ${s.onFailure === 'continue' ? 'independent tasks' : 'one chain: a failed task stops the ones after it'}.`, '');
|
|
208
|
+
s.tasks.forEach((t, i) => {
|
|
209
|
+
if (only.size > 0 && !only.has(t.id))
|
|
210
|
+
return;
|
|
211
|
+
lines.push(...taskLines(t, i));
|
|
212
|
+
});
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
lines.push('## How to answer', '', 'First tell the operator, in their language and in a few sentences, what you think happened and what you propose.', 'Then give the changes as one JSON object in a ```json block, which the operator pastes on the import page;', 'it shows each change in words before anything is done. Name sessions and tasks by the ids above.', '', '```json', '{"version": 1, "notes": "why, in one or two sentences", "changes": [', ' {"op": "continue", "session": "s-…", "task": "t-…", "instruction": "what to do now — the task goes on from where it got to, in its own chat and branch"},', ' {"op": "addTask", "session": "s-…", "after": "t-…", "task": {"title": "…", "prompt": "…", "expected": "…", "checks": [], "scope": []}}', ']}', '```', '', 'The changes, one object each:', '- "continue" {task, instruction} — a task that failed, blocked or stopped goes on from where it got to, with your prompt added to its own.', '- "newPrompt" {task, prompt} — a done task gets a new prompt that builds on its work (more to do, or done wrong).', '- "runAgain" {task, prompt?} — a task that ran starts over from where it first started, optionally with a corrected prompt; its earlier work is not used.', '- "undo" {task, how} — "this": only this task\'s work is taken out of the session\'s branch, the tasks after it kept; "back": the branch goes back to before it, and it and the tasks after it are queued again. Nothing is deleted.', '- "accept" {task, note?} — a task that did not end done is accepted as done (for example, the operator finished it by hand).', '- "addTask" {task, after?} — a new task in an existing session, after the task named (else at the end); "task" has the fields of a plan\'s task.', '- "editTask" {task, title?, prompt?, checks?, scope?} — a queued task, changed before it runs.', '- "removeTask" {task} — a queued task that is no longer wanted.', '- "note" {task, text} — information for the chat about a task, sent with its next message.', '', 'Nothing runs by itself after the import: the operator starts the run. For work that belongs in a new session, answer with a plan as before.');
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -250,7 +250,7 @@ const TaskVcsInput = z.object({
|
|
|
250
250
|
* ignored with a warning. See `importPlan`.
|
|
251
251
|
*/
|
|
252
252
|
const LegacyMirrorInput = z.object({ enabled: z.boolean().optional(), rootDir: z.string().optional() }).passthrough();
|
|
253
|
-
const TaskInput = z.object({
|
|
253
|
+
export const TaskInput = z.object({
|
|
254
254
|
title: z
|
|
255
255
|
.string()
|
|
256
256
|
.trim()
|