toolroll 0.9.58 → 0.9.60
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/CHANGELOG.md +18 -0
- package/README.md +8 -6
- package/dist/action-ledger.js +10 -1
- package/dist/assignment-adapters.d.ts +1 -1
- package/dist/assignment-adapters.js +1 -1
- package/dist/assignment-delivery.d.ts +16 -0
- package/dist/assignment-delivery.js +54 -28
- package/dist/assignment-status.d.ts +13 -4
- package/dist/assignment-status.js +70 -39
- package/dist/assignment.d.ts +6 -2
- package/dist/assignment.js +13 -8
- package/dist/bin.js +11 -2
- package/dist/brief-fence.d.ts +9 -0
- package/dist/brief-fence.js +22 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +19 -19
- package/dist/builder.js +78 -27
- package/dist/chat-channel.d.ts +1 -0
- package/dist/chat-channel.js +4 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/cli-fast.d.ts +8 -0
- package/dist/cli-fast.js +18 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.js +91 -0
- package/dist/cli.d.ts +6 -5
- package/dist/cli.js +17 -96
- package/dist/contract-changes.d.ts +52 -0
- package/dist/contract-changes.js +61 -0
- package/dist/decision.d.ts +4 -0
- package/dist/decision.js +8 -0
- package/dist/deploy-lock.d.ts +8 -0
- package/dist/deploy-lock.js +45 -0
- package/dist/design/tokens.css +234 -0
- package/dist/discord.js +2 -1
- package/dist/dispatch.js +1 -1
- package/dist/evidence.d.ts +22 -0
- package/dist/evidence.js +76 -17
- package/dist/flow-actions.js +2 -1
- package/dist/guides.js +14 -11
- package/dist/lead-cli.js +4 -1
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +8 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +11 -3
- package/dist/mailbox.js +3 -2
- package/dist/mcp-core.js +1 -1
- package/dist/needs-you.js +3 -1
- package/dist/operate.js +38 -17
- package/dist/password-bearer.d.ts +4 -4
- package/dist/password-bearer.js +2 -2
- package/dist/planner-source.d.ts +23 -53
- package/dist/planner-source.js +43 -68
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +12 -1
- package/dist/release-gate.d.ts +2 -3
- package/dist/release-gate.js +44 -16
- package/dist/scope.js +14 -0
- package/dist/serve.js +3 -2
- package/dist/server/chrome.d.ts +6 -3
- package/dist/server/chrome.js +8 -4
- package/dist/server/console.css +9 -64
- package/dist/server/render-chat.js +12 -2
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +30 -22
- package/dist/server/task-views.js +1 -0
- package/dist/server/tasks.js +3 -1
- package/dist/slack-socket.d.ts +12 -0
- package/dist/slack-socket.js +29 -0
- package/dist/slack.d.ts +0 -7
- package/dist/slack.js +2 -24
- package/dist/steer-mailbox.d.ts +39 -0
- package/dist/steer-mailbox.js +207 -0
- package/dist/steer-status.d.ts +19 -0
- package/dist/steer-status.js +30 -0
- package/dist/store.d.ts +85 -8
- package/dist/store.js +203 -15
- package/dist/surface.js +2 -2
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/task-status.js +3 -1
- package/dist/team-updates.js +3 -3
- package/dist/telegram-lead.js +6 -0
- package/dist/telegram.js +9 -7
- package/dist/toolroll-update.js +20 -1
- package/package.json +21 -21
package/dist/operate.js
CHANGED
|
@@ -10,6 +10,8 @@ import { runBatchChecks } from "./batch-checks.js";
|
|
|
10
10
|
import { BATCH_HINT, BATCH_WINDOW_MS, projectBatchChecks, setProjectBatchChecks } from "./batch-policy.js";
|
|
11
11
|
import { buildReviewPass } from "./build-review.js";
|
|
12
12
|
import { buildReviewLines, buildReviewOf } from "./review-switch.js";
|
|
13
|
+
import { steerDestinationWords, steerStandingOf, steerStandingWords } from "./steer-status.js";
|
|
14
|
+
import { utcMinute } from "./when-html.js";
|
|
13
15
|
import { parseProtectedPaths } from "./approval-policy.js";
|
|
14
16
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
15
17
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
@@ -106,7 +108,7 @@ import { spawn as spawnChild } from "node:child_process";
|
|
|
106
108
|
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
107
109
|
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
108
110
|
import { auditCommand } from "./audit-cli.js";
|
|
109
|
-
import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
|
|
111
|
+
import { closedQuestionWords, hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
|
|
110
112
|
import { readVerifiedArtifact, readVerifiedReport, storeEvidence } from "./evidence.js";
|
|
111
113
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges, encodePlannerSource, plannerSourceOf } from "./planner-source.js";
|
|
112
114
|
/**
|
|
@@ -289,7 +291,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
289
291
|
toolroll task add-tests <id> file a small task to write tests for that change
|
|
290
292
|
toolroll task revise <id> --feedback "requested change"
|
|
291
293
|
toolroll assignment show <task> root, current work and exact handoff
|
|
292
|
-
toolroll assignment updates
|
|
294
|
+
toolroll assignment updates history of handoffs sent to people (--after <cursor>)
|
|
293
295
|
toolroll assignment brief catch up from the local database
|
|
294
296
|
toolroll assignment inbox --consumer <name> receive saved status changes
|
|
295
297
|
toolroll assignment ack --consumer <name> --batch <id> acknowledge delivery
|
|
@@ -306,9 +308,10 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
306
308
|
is still required); --undo puts it
|
|
307
309
|
back in filing order
|
|
308
310
|
toolroll task steer <id> --note "..."
|
|
309
|
-
guidance for the
|
|
310
|
-
reads
|
|
311
|
-
|
|
311
|
+
guidance for the agent — a running
|
|
312
|
+
build reads it when it next checks
|
|
313
|
+
for notes, otherwise the next
|
|
314
|
+
attempt does
|
|
312
315
|
toolroll task assign <id> --runner <name> | --anyone
|
|
313
316
|
reserve it for one worker (it joins
|
|
314
317
|
the back of that worker's queue) or
|
|
@@ -3721,7 +3724,7 @@ async function decideCommand(positional, flags, context) {
|
|
|
3721
3724
|
write(envelopeJson({ ok: true, command: "decide", decision, taskId, evidence }));
|
|
3722
3725
|
return EXIT.ok;
|
|
3723
3726
|
}
|
|
3724
|
-
write(`${taskId} — ${decision.state.toUpperCase()}${decision.deadline === null ? "" : ` · deadline ${decision.deadline}`}`);
|
|
3727
|
+
write(`${taskId} — ${decision.supersededReason !== null ? "CLOSED" : decision.state.toUpperCase()}${decision.deadline === null ? "" : ` · deadline ${decision.deadline}`}`);
|
|
3725
3728
|
write("");
|
|
3726
3729
|
write(` ${decision.recap}`);
|
|
3727
3730
|
write("");
|
|
@@ -3739,7 +3742,9 @@ async function decideCommand(positional, flags, context) {
|
|
|
3739
3742
|
}
|
|
3740
3743
|
if (decision.state === "answered") {
|
|
3741
3744
|
write("");
|
|
3742
|
-
write(
|
|
3745
|
+
write(decision.supersededReason !== null
|
|
3746
|
+
? ` ${closedQuestionWords(decision.supersededReason)} (${decision.answeredAt})`
|
|
3747
|
+
: ` answered: ${decision.choice} by ${decision.answeredBy} at ${decision.answeredAt}${decision.note === null ? "" : ` — ${decision.note}`}`);
|
|
3743
3748
|
}
|
|
3744
3749
|
if (evidence.length > 0) {
|
|
3745
3750
|
write("");
|
|
@@ -3747,8 +3752,10 @@ async function decideCommand(positional, flags, context) {
|
|
|
3747
3752
|
write(` evidence ${artifact.kind.padEnd(14)} ${artifact.key}${artifact.truncated ? " (truncated)" : ""}`);
|
|
3748
3753
|
}
|
|
3749
3754
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3755
|
+
if (decision.state !== "answered") {
|
|
3756
|
+
write("");
|
|
3757
|
+
write(` → toolroll decide ${id} --choose <option> --as <you> --token <t>`);
|
|
3758
|
+
}
|
|
3752
3759
|
return EXIT.ok;
|
|
3753
3760
|
}
|
|
3754
3761
|
const acting = await askCredentials(flags, context);
|
|
@@ -3767,9 +3774,11 @@ async function decideCommand(positional, flags, context) {
|
|
|
3767
3774
|
? `"${choice}" is not one of this decision's options — \`toolroll decide ${id}\` shows them`
|
|
3768
3775
|
: answered.reason === "already-answered"
|
|
3769
3776
|
? `decision ${id} was already answered differently — "decided" is not negotiable; park a new task if the answer must change`
|
|
3770
|
-
: answered.reason === "
|
|
3771
|
-
?
|
|
3772
|
-
:
|
|
3777
|
+
: answered.reason === "closed"
|
|
3778
|
+
? `decision ${id}: ${closedQuestionWords(store.getDecision(id)?.supersededReason ?? "cancelled")} Nothing to answer.`
|
|
3779
|
+
: answered.reason === "bad-note"
|
|
3780
|
+
? "the note is too long or carries control characters"
|
|
3781
|
+
: `no decision ${id}`;
|
|
3773
3782
|
return fail(write, json, "decide", answered.reason, why, EXIT.refused);
|
|
3774
3783
|
}
|
|
3775
3784
|
return succeed(write, json, "decide", { decision: answered.decision, duplicate: answered.duplicate === true }, () => answered.duplicate === true
|
|
@@ -8999,6 +9008,7 @@ function showTask(positional, context) {
|
|
|
8999
9008
|
const listed = workIndexTask(store, id, now, access, context.evidenceRoot);
|
|
9000
9009
|
const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
|
|
9001
9010
|
const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
|
|
9011
|
+
const steeringLive = store.steeringLiveBuild(ref.id, now) !== null;
|
|
9002
9012
|
const detail = {
|
|
9003
9013
|
task,
|
|
9004
9014
|
work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
|
|
@@ -9038,6 +9048,9 @@ function showTask(positional, context) {
|
|
|
9038
9048
|
stops: store.stopsForTask(ref.id),
|
|
9039
9049
|
// The pull request Complete opened (any version), with its CI state and merge commit — the console's view.
|
|
9040
9050
|
pullRequest: newestPullRequestOf(store, store.taskFamilyOf(id, null, true)?.versions.map(one => one.id) ?? [id]),
|
|
9051
|
+
// Operator steering (v120), in the task page's words: read (and when), waiting, or for the next attempt.
|
|
9052
|
+
steering: store.listSteerNotes(ref.id).map(one => ({ id: one.id, author: one.author, createdAt: one.createdAt, note: one.note,
|
|
9053
|
+
standing: steerStandingOf(one, steeringLive), words: steerStandingWords(one, steeringLive, utcMinute) })),
|
|
9041
9054
|
};
|
|
9042
9055
|
return succeed(write, json, "task show", detail, () => [
|
|
9043
9056
|
`${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
|
|
@@ -9072,6 +9085,7 @@ function showTask(positional, context) {
|
|
|
9072
9085
|
? []
|
|
9073
9086
|
: [` position ${detail.position.position} of ${detail.position.total}${detail.position.column === null ? " in the shared queue" : ` in ${detail.position.column}'s queue`}`]),
|
|
9074
9087
|
...(detail.hold === null ? [] : [` held: ${detail.hold.reason}`]),
|
|
9088
|
+
...detail.steering.slice(-5).map(one => ` note #${one.id} to the agent: ${one.words}`),
|
|
9075
9089
|
...(detail.claim === null ? [] : [` claimed by ${detail.claim.runner} until ${detail.claim.expiresAt}`]),
|
|
9076
9090
|
...(detail.dispatch === null ? [] : [` dispatch: ${detail.dispatch.summary} — ${detail.dispatch.detail}`]),
|
|
9077
9091
|
...(detail.control.kind === "none"
|
|
@@ -9427,7 +9441,7 @@ async function steerTask(positional, flags, context) {
|
|
|
9427
9441
|
const id = positional[0];
|
|
9428
9442
|
const note = text(flags, "note");
|
|
9429
9443
|
if (id === undefined || note === undefined) {
|
|
9430
|
-
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; the note
|
|
9444
|
+
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; a running build's agent reads the note when it next checks for notes, otherwise the next attempt's brief quotes it, fenced, inside the approved scope", EXIT.usage);
|
|
9431
9445
|
}
|
|
9432
9446
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9433
9447
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
@@ -9449,8 +9463,10 @@ async function steerTask(positional, flags, context) {
|
|
|
9449
9463
|
: (filed.problem ?? "that note will not store");
|
|
9450
9464
|
return fail(write, json, "task steer", filed.reason, detail, EXIT.refused);
|
|
9451
9465
|
}
|
|
9452
|
-
|
|
9453
|
-
|
|
9466
|
+
const steered = store.lookupRef(id);
|
|
9467
|
+
const live = steered !== null && store.steeringLiveBuild(steered.id, context.now) !== null;
|
|
9468
|
+
return succeed(write, json, "task steer", { task: id, note: filed.id, standing: live ? "waiting" : "next-attempt" }, () => [
|
|
9469
|
+
`Noted. ${steerDestinationWords(live)} \`task show ${id}\` says when it has been read.`,
|
|
9454
9470
|
]);
|
|
9455
9471
|
}
|
|
9456
9472
|
/** The mirror of block: stop waiting. Removal cannot create a cycle. */
|
|
@@ -9707,6 +9723,7 @@ async function scopeTask(positional, flags, context) {
|
|
|
9707
9723
|
// the mode seal record as a single operation —
|
|
9708
9724
|
// a replayed key returns the FIRST answer whole instead of re-sealing
|
|
9709
9725
|
// whatever scope happens to be current.
|
|
9726
|
+
const generationBefore = store.lookupRef(id)?.planningGeneration ?? 0;
|
|
9710
9727
|
const filed = store.replay(mutationFrom(flags, now), "task-scope-filed", () => store.transact(() => {
|
|
9711
9728
|
const coverage = actor !== null
|
|
9712
9729
|
? modeFilingCoverage(store, store.refFor(BUILT_IN, id).repo, actor, now)
|
|
@@ -9763,8 +9780,12 @@ async function scopeTask(positional, flags, context) {
|
|
|
9763
9780
|
...describeScope(scope),
|
|
9764
9781
|
]);
|
|
9765
9782
|
}
|
|
9766
|
-
|
|
9767
|
-
|
|
9783
|
+
// A changed scope on a planning task plans again (v119): say so once, instead of leaving the old plan implied.
|
|
9784
|
+
const replanned = (store.lookupRef(id)?.planningGeneration ?? 0) > generationBefore;
|
|
9785
|
+
return succeed(write, json, "task scope", { scope, ...(replanned ? { planningRestarted: true } : {}) }, () => [
|
|
9786
|
+
replanned
|
|
9787
|
+
? `Scope written for ${id}. The scope changed, so planning starts again; nothing builds until somebody approves the new plan.`
|
|
9788
|
+
: `Scope written for ${id}. Nothing will build it until somebody approves it.`,
|
|
9768
9789
|
...describeScope(scope),
|
|
9769
9790
|
"",
|
|
9770
9791
|
` toolroll task approve ${id} --yes`,
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
import type { ServerResponse } from "node:http";
|
|
8
8
|
/** The one replacement every machine-credential refusal names. */
|
|
9
9
|
export declare const TOKEN_REPLACEMENT = "Create an API token with `toolroll tokens create`";
|
|
10
|
-
/** The first release that refuses a password on a request
|
|
11
|
-
export declare const PASSWORD_BEARER_REFUSED_FROM = "0.
|
|
12
|
-
export declare const PASSWORD_BEARER_WARNING = "Signing in with a password on each request is deprecated and stops working in Toolroll 0.
|
|
10
|
+
/** The first release that refuses a password on a request; 0.9.58 deprecated it with a warning. */
|
|
11
|
+
export declare const PASSWORD_BEARER_REFUSED_FROM = "0.10.0";
|
|
12
|
+
export declare const PASSWORD_BEARER_WARNING = "Signing in with a password on each request is deprecated and stops working in Toolroll 0.10.0. Create an API token with `toolroll tokens create` and send it as the bearer instead.";
|
|
13
13
|
/** What the CLI says on stderr when a saved connection signs in with a password instead of an API token. */
|
|
14
|
-
export declare const PASSWORD_PROFILE_WARNING = "Warning: this connection signs in with your password on each request, which is deprecated and stops working in Toolroll 0.
|
|
14
|
+
export declare const PASSWORD_PROFILE_WARNING = "Warning: this connection signs in with your password on each request, which is deprecated and stops working in Toolroll 0.10.0. Create an API token with `toolroll tokens create`, then connect again with --token-stdin.";
|
|
15
15
|
export declare const PASSWORD_BEARER_REFUSED = "Toolroll no longer accepts a password on a request. Create an API token with `toolroll tokens create` and send it as the bearer instead.";
|
|
16
16
|
/** Whether an Authorization header is a password bearer: a name and a secret, never an API token. */
|
|
17
17
|
export declare function isPasswordBearer(authorization: string | undefined): boolean;
|
package/dist/password-bearer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "./version.js";
|
|
2
2
|
/** The one replacement every machine-credential refusal names. */
|
|
3
3
|
export const TOKEN_REPLACEMENT = "Create an API token with `toolroll tokens create`";
|
|
4
|
-
/** The first release that refuses a password on a request
|
|
5
|
-
export const PASSWORD_BEARER_REFUSED_FROM = "0.
|
|
4
|
+
/** The first release that refuses a password on a request; 0.9.58 deprecated it with a warning. */
|
|
5
|
+
export const PASSWORD_BEARER_REFUSED_FROM = "0.10.0";
|
|
6
6
|
export const PASSWORD_BEARER_WARNING = `Signing in with a password on each request is deprecated and stops working in Toolroll ${PASSWORD_BEARER_REFUSED_FROM}. ${TOKEN_REPLACEMENT} and send it as the bearer instead.`;
|
|
7
7
|
/** What the CLI says on stderr when a saved connection signs in with a password instead of an API token. */
|
|
8
8
|
export const PASSWORD_PROFILE_WARNING = `Warning: this connection signs in with your password on each request, which is deprecated and stops working in Toolroll ${PASSWORD_BEARER_REFUSED_FROM}. ${TOKEN_REPLACEMENT}, then connect again with --token-stdin.`;
|
package/dist/planner-source.d.ts
CHANGED
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
* transactions of its own: the fenced finalizers in claim.ts seal.
|
|
37
37
|
*/
|
|
38
38
|
import type { Store } from "./store.js";
|
|
39
|
-
import type {
|
|
39
|
+
import type { Scope } from "./scope.js";
|
|
40
40
|
import type { QualityMode } from "./quality.js";
|
|
41
41
|
import type { RiskLevel } from "./phase-routing.js";
|
|
42
|
+
import { type ContractChange, type ContractTerms } from "./contract-changes.js";
|
|
42
43
|
export declare const PLANNER_SOURCE_VERSION = 1;
|
|
43
44
|
export declare const PLANNER_SOURCE_LIMITS: {
|
|
44
45
|
/** The whole recorded source before lossless prompt fencing. Equal to
|
|
@@ -49,13 +50,9 @@ export declare const PLANNER_SOURCE_LIMITS: {
|
|
|
49
50
|
readonly amendment: 1000;
|
|
50
51
|
/** Earlier answers quoted into the brief. */
|
|
51
52
|
readonly answers: 5;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
goal: string;
|
|
56
|
-
outOfScope: string | null;
|
|
57
|
-
touches: string[];
|
|
58
|
-
acceptance: AcceptanceCriterion[];
|
|
53
|
+
/** Terms that exceed the entire handoff cap cannot fit regardless of the plan's size. Do not reserve a maximum-
|
|
54
|
+
* sized plan here: a shorter document can carry larger valid terms. The final payload still enforces its byte cap. */
|
|
55
|
+
readonly handoffTerms: number;
|
|
59
56
|
};
|
|
60
57
|
export type PlannerContractScope = ContractTerms & {
|
|
61
58
|
/** The filed row's digest — the exact bytes the terms carry today. */
|
|
@@ -154,7 +151,7 @@ export type PlannerSourceResult = {
|
|
|
154
151
|
bytes: number;
|
|
155
152
|
} | {
|
|
156
153
|
ok: false;
|
|
157
|
-
reason: "no-task" | "revision-brief" | "oversized";
|
|
154
|
+
reason: "no-task" | "revision-brief" | "oversized" | "too-long";
|
|
158
155
|
message: string;
|
|
159
156
|
};
|
|
160
157
|
/**
|
|
@@ -168,55 +165,28 @@ export declare function plannerSourceOf(store: Store, root: string, taskId: stri
|
|
|
168
165
|
/**
|
|
169
166
|
* The dispatch diagnosis's read of the same gate, from the store alone:
|
|
170
167
|
* why the next pass would refuse to plan this task before spending — the
|
|
171
|
-
* revision brief unreadable,
|
|
172
|
-
* size is a floor (the brief's
|
|
173
|
-
* "yes" here is certain and the
|
|
174
|
-
* word. Null when nothing here
|
|
168
|
+
* revision brief unreadable, the filed request over the byte cap, or terms
|
|
169
|
+
* the planner's handoff cannot carry. The size is a floor (the brief's
|
|
170
|
+
* stored bytes, before JSON quoting), so a "yes" here is certain and the
|
|
171
|
+
* dispatch refusal itself stays the exact word. Null when nothing here
|
|
172
|
+
* stands in the way.
|
|
173
|
+
*/
|
|
174
|
+
export declare function plannerSourceProblemOf(store: Store, taskId: string): {
|
|
175
|
+
title: string;
|
|
176
|
+
detail: string;
|
|
177
|
+
} | null;
|
|
178
|
+
/**
|
|
179
|
+
* Whether the planner can hand the filed terms back (contracts/plan.ts): it must reproduce the goal, exclusions,
|
|
180
|
+
* touches and criteria in its handoff, so terms its handoff cannot carry would end every attempt as a malformed plan.
|
|
181
|
+
* This names the problem before any workspace, run or provider spend instead: the actual size, the limit, and what to
|
|
182
|
+
* shorten. Null when the terms fit.
|
|
175
183
|
*/
|
|
176
|
-
export declare function
|
|
184
|
+
export declare function plannerHandoffProblemOf(terms: ContractTerms): string | null;
|
|
177
185
|
/** The exact bytes recorded as evidence; prompt fencing preserves the JSON value. */
|
|
178
186
|
export declare function encodePlannerSource(source: PlannerSource): Buffer;
|
|
179
187
|
/** Read a recorded source back; null when the bytes are not one. */
|
|
180
188
|
export declare function decodePlannerSource(content: Buffer): PlannerSource | null;
|
|
181
|
-
export type ContractChange
|
|
182
|
-
field: "goal";
|
|
183
|
-
kind: "changed";
|
|
184
|
-
before: string;
|
|
185
|
-
after: string;
|
|
186
|
-
} | {
|
|
187
|
-
field: "outOfScope";
|
|
188
|
-
kind: "added" | "removed" | "changed";
|
|
189
|
-
before: string | null;
|
|
190
|
-
after: string | null;
|
|
191
|
-
} | {
|
|
192
|
-
field: "touches";
|
|
193
|
-
kind: "added" | "removed";
|
|
194
|
-
path: string;
|
|
195
|
-
} | {
|
|
196
|
-
field: "acceptance";
|
|
197
|
-
kind: "added" | "removed" | "changed";
|
|
198
|
-
id: string;
|
|
199
|
-
before: {
|
|
200
|
-
statement: string;
|
|
201
|
-
evidence: EvidenceKind[];
|
|
202
|
-
how: string | null;
|
|
203
|
-
} | null;
|
|
204
|
-
after: {
|
|
205
|
-
statement: string;
|
|
206
|
-
evidence: EvidenceKind[];
|
|
207
|
-
how: string | null;
|
|
208
|
-
} | null;
|
|
209
|
-
/** For `changed`: which parts moved. `how` is advisory and unsigned,
|
|
210
|
-
* but a planner rewriting it is still a change the operator sees. */
|
|
211
|
-
moved: ("statement" | "evidence" | "how")[];
|
|
212
|
-
};
|
|
213
|
-
/**
|
|
214
|
-
* Every addition, change, and removal between the filed contract and a
|
|
215
|
-
* proposed one. Whitespace at the ends and touch/evidence order do not
|
|
216
|
-
* count (the scope digest ignores them too); everything else does,
|
|
217
|
-
* including the advisory `how`, because the operator wrote it.
|
|
218
|
-
*/
|
|
219
|
-
export declare function contractChangesOf(filed: ContractTerms, proposed: ContractTerms): ContractChange[];
|
|
189
|
+
export { contractChangesOf, type ContractChange, type ContractTerms } from "./contract-changes.js";
|
|
220
190
|
/** One line per change, for terminals, notifications, and validation messages. */
|
|
221
191
|
export declare function describeContractChanges(changes: readonly ContractChange[]): string[];
|
|
222
192
|
/**
|
package/dist/planner-source.js
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
import { createHash } from "node:crypto";
|
|
39
39
|
import { EVIDENCE_CAPS, readVerifiedArtifact } from "./evidence.js";
|
|
40
40
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
41
|
+
import { PLAN_TOUCHES } from "./contracts/plan.js";
|
|
42
|
+
import { contractChangesOf } from "./contract-changes.js";
|
|
41
43
|
export const PLANNER_SOURCE_VERSION = 1;
|
|
42
44
|
export const PLANNER_SOURCE_LIMITS = {
|
|
43
45
|
/** The whole recorded source before lossless prompt fencing. Equal to
|
|
@@ -48,6 +50,9 @@ export const PLANNER_SOURCE_LIMITS = {
|
|
|
48
50
|
amendment: TEXT_LIMITS.planAmendment,
|
|
49
51
|
/** Earlier answers quoted into the brief. */
|
|
50
52
|
answers: 5,
|
|
53
|
+
/** Terms that exceed the entire handoff cap cannot fit regardless of the plan's size. Do not reserve a maximum-
|
|
54
|
+
* sized plan here: a shorter document can carry larger valid terms. The final payload still enforces its byte cap. */
|
|
55
|
+
handoffTerms: TEXT_LIMITS.planPayloadBytes,
|
|
51
56
|
};
|
|
52
57
|
/** Stable JSON for hashing and equality only — no filling, no trimming. */
|
|
53
58
|
export function canonicalContractJson(value) {
|
|
@@ -192,26 +197,55 @@ export function plannerSourceOf(store, root, taskId, answers) {
|
|
|
192
197
|
`(scope ${scopeBytes}, revision brief ${briefBytes}); nothing is trimmed silently — shorten the scope or brief, then plan again`,
|
|
193
198
|
};
|
|
194
199
|
}
|
|
200
|
+
const handoff = source.contract.scope === null ? null : plannerHandoffProblemOf(source.contract.scope);
|
|
201
|
+
if (handoff !== null)
|
|
202
|
+
return { ok: false, reason: "too-long", message: handoff };
|
|
195
203
|
return { ok: true, source, bytes };
|
|
196
204
|
}
|
|
197
205
|
/**
|
|
198
206
|
* The dispatch diagnosis's read of the same gate, from the store alone:
|
|
199
207
|
* why the next pass would refuse to plan this task before spending — the
|
|
200
|
-
* revision brief unreadable,
|
|
201
|
-
* size is a floor (the brief's
|
|
202
|
-
* "yes" here is certain and the
|
|
203
|
-
* word. Null when nothing here
|
|
208
|
+
* revision brief unreadable, the filed request over the byte cap, or terms
|
|
209
|
+
* the planner's handoff cannot carry. The size is a floor (the brief's
|
|
210
|
+
* stored bytes, before JSON quoting), so a "yes" here is certain and the
|
|
211
|
+
* dispatch refusal itself stays the exact word. Null when nothing here
|
|
212
|
+
* stands in the way.
|
|
204
213
|
*/
|
|
205
214
|
export function plannerSourceProblemOf(store, taskId) {
|
|
215
|
+
const filed = (detail) => ({ title: "The filed request cannot be planned as filed", detail });
|
|
206
216
|
if (store.answeredDecisionsFor(taskId, PLANNER_SOURCE_LIMITS.answers + 1).length > PLANNER_SOURCE_LIMITS.answers)
|
|
207
|
-
return `more than ${PLANNER_SOURCE_LIMITS.answers} answered decisions exceed the planner history bound; file a follow-up task with the consolidated decisions
|
|
217
|
+
return filed(`more than ${PLANNER_SOURCE_LIMITS.answers} answered decisions exceed the planner history bound; file a follow-up task with the consolidated decisions`);
|
|
208
218
|
const contract = plannerContractOf(store, taskId);
|
|
209
219
|
if (!contract.ok)
|
|
210
|
-
return contract.message;
|
|
220
|
+
return filed(contract.message);
|
|
211
221
|
const briefBytes = contract.contract.revision === null ? 0 : (store.getArtifact(contract.contract.revision.briefArtifact)?.bytesStored ?? 0);
|
|
212
222
|
const floor = Buffer.byteLength(JSON.stringify(contract.contract, null, 2), "utf8") + briefBytes;
|
|
213
223
|
if (floor > PLANNER_SOURCE_LIMITS.bytes) {
|
|
214
|
-
return `the filed request is at least ${floor} bytes — over the ${PLANNER_SOURCE_LIMITS.bytes}-byte planner source cap; shorten the scope or brief before planning
|
|
224
|
+
return filed(`the filed request is at least ${floor} bytes — over the ${PLANNER_SOURCE_LIMITS.bytes}-byte planner source cap; shorten the scope or brief before planning`);
|
|
225
|
+
}
|
|
226
|
+
const handoff = contract.contract.scope === null ? null : plannerHandoffProblemOf(contract.contract.scope);
|
|
227
|
+
return handoff === null ? null : { title: "Scope too long to plan", detail: handoff };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Whether the planner can hand the filed terms back (contracts/plan.ts): it must reproduce the goal, exclusions,
|
|
231
|
+
* touches and criteria in its handoff, so terms its handoff cannot carry would end every attempt as a malformed plan.
|
|
232
|
+
* This names the problem before any workspace, run or provider spend instead: the actual size, the limit, and what to
|
|
233
|
+
* shorten. Null when the terms fit.
|
|
234
|
+
*/
|
|
235
|
+
export function plannerHandoffProblemOf(terms) {
|
|
236
|
+
const n = (value) => value.toLocaleString("en-US");
|
|
237
|
+
for (const [label, is, text] of [["goal", "is", terms.goal], ["exclusions", "are", terms.outOfScope ?? ""]]) {
|
|
238
|
+
if (text.length > TEXT_LIMITS.goal)
|
|
239
|
+
return `The ${label} ${is} ${n(text.length)} characters; the planner can carry at most ${n(TEXT_LIMITS.goal)}. Shorten the ${label} and planning starts again.`;
|
|
240
|
+
}
|
|
241
|
+
if (terms.touches.length > PLAN_TOUCHES)
|
|
242
|
+
return `The scope lists ${n(terms.touches.length)} paths; the planner can carry at most ${PLAN_TOUCHES}. List fewer paths and planning starts again.`;
|
|
243
|
+
const path = terms.touches.find(one => one.length > TEXT_LIMITS.planTouch);
|
|
244
|
+
if (path !== undefined)
|
|
245
|
+
return `A path is ${n(path.length)} characters; the planner can carry at most ${n(TEXT_LIMITS.planTouch)}. Shorten it and planning starts again.`;
|
|
246
|
+
const bytes = Buffer.byteLength(JSON.stringify({ goal: terms.goal, outOfScope: terms.outOfScope, touches: terms.touches, acceptance: terms.acceptance }), "utf8");
|
|
247
|
+
if (bytes > PLANNER_SOURCE_LIMITS.handoffTerms) {
|
|
248
|
+
return `The goal, exclusions, paths and criteria are ${n(bytes)} bytes together; the planner can carry at most ${n(PLANNER_SOURCE_LIMITS.handoffTerms)} bytes in its whole handoff. Shorten the scope and planning starts again.`;
|
|
215
249
|
}
|
|
216
250
|
return null;
|
|
217
251
|
}
|
|
@@ -257,67 +291,8 @@ export function decodePlannerSource(content) {
|
|
|
257
291
|
return null;
|
|
258
292
|
}
|
|
259
293
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return null;
|
|
263
|
-
const t = text.trim();
|
|
264
|
-
return t === "" ? null : t;
|
|
265
|
-
};
|
|
266
|
-
/**
|
|
267
|
-
* Every addition, change, and removal between the filed contract and a
|
|
268
|
-
* proposed one. Whitespace at the ends and touch/evidence order do not
|
|
269
|
-
* count (the scope digest ignores them too); everything else does,
|
|
270
|
-
* including the advisory `how`, because the operator wrote it.
|
|
271
|
-
*/
|
|
272
|
-
export function contractChangesOf(filed, proposed) {
|
|
273
|
-
const changes = [];
|
|
274
|
-
if (filed.goal.trim() !== proposed.goal.trim()) {
|
|
275
|
-
changes.push({ field: "goal", kind: "changed", before: filed.goal, after: proposed.goal });
|
|
276
|
-
}
|
|
277
|
-
const filedOut = trimmed(filed.outOfScope);
|
|
278
|
-
const proposedOut = trimmed(proposed.outOfScope);
|
|
279
|
-
if (filedOut !== proposedOut) {
|
|
280
|
-
changes.push({
|
|
281
|
-
field: "outOfScope",
|
|
282
|
-
kind: filedOut === null ? "added" : proposedOut === null ? "removed" : "changed",
|
|
283
|
-
before: filed.outOfScope,
|
|
284
|
-
after: proposed.outOfScope,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
const filedTouches = new Set(filed.touches.map(one => one.trim()).filter(one => one !== ""));
|
|
288
|
-
const proposedTouches = new Set(proposed.touches.map(one => one.trim()).filter(one => one !== ""));
|
|
289
|
-
for (const path of filedTouches)
|
|
290
|
-
if (!proposedTouches.has(path))
|
|
291
|
-
changes.push({ field: "touches", kind: "removed", path });
|
|
292
|
-
for (const path of proposedTouches)
|
|
293
|
-
if (!filedTouches.has(path))
|
|
294
|
-
changes.push({ field: "touches", kind: "added", path });
|
|
295
|
-
const criterionOf = (one) => ({ statement: one.statement, evidence: [...one.evidence].sort(), how: one.how });
|
|
296
|
-
const filedById = new Map(filed.acceptance.map(one => [one.id, one]));
|
|
297
|
-
const proposedById = new Map(proposed.acceptance.map(one => [one.id, one]));
|
|
298
|
-
for (const [id, before] of filedById) {
|
|
299
|
-
const after = proposedById.get(id);
|
|
300
|
-
if (after === undefined) {
|
|
301
|
-
changes.push({ field: "acceptance", kind: "removed", id, before: criterionOf(before), after: null, moved: [] });
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
const moved = [];
|
|
305
|
-
if (before.statement.trim() !== after.statement.trim())
|
|
306
|
-
moved.push("statement");
|
|
307
|
-
if ([...before.evidence].sort().join(",") !== [...after.evidence].sort().join(","))
|
|
308
|
-
moved.push("evidence");
|
|
309
|
-
if (trimmed(before.how) !== trimmed(after.how))
|
|
310
|
-
moved.push("how");
|
|
311
|
-
if (moved.length > 0) {
|
|
312
|
-
changes.push({ field: "acceptance", kind: "changed", id, before: criterionOf(before), after: criterionOf(after), moved });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
for (const [id, after] of proposedById) {
|
|
316
|
-
if (!filedById.has(id))
|
|
317
|
-
changes.push({ field: "acceptance", kind: "added", id, before: null, after: criterionOf(after), moved: [] });
|
|
318
|
-
}
|
|
319
|
-
return changes;
|
|
320
|
-
}
|
|
294
|
+
// ---- amendments -----------------------------------------------------------
|
|
295
|
+
export { contractChangesOf } from "./contract-changes.js";
|
|
321
296
|
const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
|
|
322
297
|
/** One line per change, for terminals, notifications, and validation messages. */
|
|
323
298
|
export function describeContractChanges(changes) {
|
package/dist/provider.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ export type Invocation = {
|
|
|
64
64
|
* Codex renders it as its own sandbox profile; Claude as deny rules for
|
|
65
65
|
* its file tools (its shell is fenced by the macOS sandbox around it). */
|
|
66
66
|
fence?: readonly string[];
|
|
67
|
+
/** A running build's steering folder (steer-mailbox.ts): the absolute path
|
|
68
|
+
* and the worktree-relative name its brief uses. Claude renders a
|
|
69
|
+
* PostToolUse hook that says when a note is waiting; every other provider
|
|
70
|
+
* relies on the brief's own rule to look. */
|
|
71
|
+
steerFolder?: {
|
|
72
|
+
dir: string;
|
|
73
|
+
name: string;
|
|
74
|
+
};
|
|
67
75
|
/** Claude's structured-output floor for a non-review phase: the scout's
|
|
68
76
|
* report rides the terminal result event instead of a file plan mode
|
|
69
77
|
* would refuse to write. Ignored by providers without `--json-schema`. */
|
package/dist/provider.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* shell the model itself launches (Codex provider review, Q5).
|
|
20
20
|
*/
|
|
21
21
|
import { claudeFenceSettings, codexFenceArgv } from "./agent-fence.js";
|
|
22
|
+
import { claudeSteerHooks } from "./steer-mailbox.js";
|
|
22
23
|
import {} from "./exec.js";
|
|
23
24
|
import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl } from "./exec.js";
|
|
24
25
|
import { scanForSecrets } from "./evidence.js";
|
|
@@ -198,10 +199,20 @@ const claudeArgv = (invocation) => [
|
|
|
198
199
|
...(invocation.allowedTools === undefined || invocation.allowedTools.length === 0 ? [] : ["--allowedTools", invocation.allowedTools.join(",")]),
|
|
199
200
|
...(invocation.toolArgv ?? []),
|
|
200
201
|
...(invocation.extraMcpServers === undefined ? [] : ["--mcp-config", JSON.stringify({ mcpServers: invocation.extraMcpServers })]),
|
|
201
|
-
...(invocation
|
|
202
|
+
...claudeSettingsArgv(invocation),
|
|
202
203
|
...(invocation.jsonSchema === undefined ? [] : ["--json-schema", JSON.stringify(invocation.jsonSchema)])
|
|
203
204
|
]),
|
|
204
205
|
];
|
|
206
|
+
/** One --settings for a build: the fence's deny rules and the steering doorbell, whichever the run has. */
|
|
207
|
+
function claudeSettingsArgv(invocation) {
|
|
208
|
+
const fenced = invocation.fence !== undefined && invocation.fence.length > 0;
|
|
209
|
+
if (!fenced && invocation.steerFolder === undefined)
|
|
210
|
+
return [];
|
|
211
|
+
return ["--settings", JSON.stringify({
|
|
212
|
+
...(fenced ? JSON.parse(claudeFenceSettings(invocation.fence)) : {}),
|
|
213
|
+
...(invocation.steerFolder === undefined ? {} : { hooks: claudeSteerHooks(invocation.steerFolder.dir, invocation.steerFolder.name) }),
|
|
214
|
+
})];
|
|
215
|
+
}
|
|
205
216
|
/**
|
|
206
217
|
* The turn's own words, preferring the schema-validated structured field
|
|
207
218
|
* (present only under `--json-schema`) over the plain result string — the
|
package/dist/release-gate.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `toolroll release --release-gate <id>`: lift one update pause that a browser deployment (scripts/deploy-browser.mjs)
|
|
3
|
-
* left behind when it stopped before the swap and its own recovery
|
|
4
|
-
* killed
|
|
5
|
-
* `--phase recover` leaves the pause alone, and new work stays paused.
|
|
3
|
+
* left behind when it stopped before the swap and its own exit recovery never ran. The typical case is a deployment
|
|
4
|
+
* killed outright (SIGKILL, a crash or power loss) while paused: nothing lifted the pause, and new work stays paused.
|
|
6
5
|
*
|
|
7
6
|
* The id alone proves nothing, and neither does the journal's phase. The pause is lifted only when every piece of
|
|
8
7
|
* evidence agrees that the service was never swapped:
|
package/dist/release-gate.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `toolroll release --release-gate <id>`: lift one update pause that a browser deployment (scripts/deploy-browser.mjs)
|
|
3
|
-
* left behind when it stopped before the swap and its own recovery
|
|
4
|
-
* killed
|
|
5
|
-
* `--phase recover` leaves the pause alone, and new work stays paused.
|
|
3
|
+
* left behind when it stopped before the swap and its own exit recovery never ran. The typical case is a deployment
|
|
4
|
+
* killed outright (SIGKILL, a crash or power loss) while paused: nothing lifted the pause, and new work stays paused.
|
|
6
5
|
*
|
|
7
6
|
* The id alone proves nothing, and neither does the journal's phase. The pause is lifted only when every piece of
|
|
8
7
|
* evidence agrees that the service was never swapped:
|
|
@@ -23,6 +22,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
23
22
|
import { join } from "node:path";
|
|
24
23
|
import { durableJson } from "./desktop-update.js";
|
|
25
24
|
import { removeUpdateGate, updateGateOwned } from "./desktop-update-gate.js";
|
|
25
|
+
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
26
26
|
/** The journal phases a deployment passes before it stops the old service (deploy-phases.mjs). */
|
|
27
27
|
export const BEFORE_SWAP = Object.freeze(["preparing", "admission-paused", "frozen", "backup-verified", "rehearsed"]);
|
|
28
28
|
/** Journal fields that only the swap and later phases write. */
|
|
@@ -129,22 +129,50 @@ export function proveBeforeSwap(id, world) {
|
|
|
129
129
|
}
|
|
130
130
|
/** Prove, then (with `yes`) lift exactly that pause and journal the release in the deployment's own journal. */
|
|
131
131
|
export function releaseGate(id, world, yes) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
// Discovery is not authority to remove anything. Dry-run remains read-only, even for legacy stages.
|
|
133
|
+
const discovered = proveBeforeSwap(id, world);
|
|
134
|
+
if (!discovered.ok)
|
|
135
|
+
return discovered;
|
|
135
136
|
if (!yes)
|
|
136
|
-
return { ok: true, released: false, id, stage:
|
|
137
|
-
const
|
|
138
|
-
|
|
137
|
+
return { ok: true, released: false, id, stage: discovered.stage, proof: discovered.proof };
|
|
138
|
+
const journalFile = join(discovered.stage, "deployment.json");
|
|
139
|
+
let lock;
|
|
139
140
|
try {
|
|
140
|
-
|
|
141
|
+
lock = acquireDeploymentLock(journalFile);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return refuse("deploying", error.message);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const proved = proveBeforeSwap(id, world);
|
|
148
|
+
if (!proved.ok)
|
|
149
|
+
return proved;
|
|
150
|
+
if (proved.stage !== discovered.stage)
|
|
151
|
+
return refuse("conflict", "The deployment stage changed during release.");
|
|
152
|
+
const db = world.open();
|
|
153
|
+
try {
|
|
154
|
+
// Repeat the entire proof at the mutation boundary, under the same exclusion as deploy/resume/recover.
|
|
155
|
+
const fresh = proveBeforeSwap(id, world);
|
|
156
|
+
if (!fresh.ok)
|
|
157
|
+
return fresh;
|
|
158
|
+
if (fresh.stage !== proved.stage || JSON.stringify(fresh.journal) !== JSON.stringify(proved.journal) ||
|
|
159
|
+
JSON.stringify(JSON.parse(readFileSync(journalFile, "utf8"))) !== JSON.stringify(fresh.journal)) {
|
|
160
|
+
return refuse("conflict", "The deployment journal changed during release.");
|
|
161
|
+
}
|
|
162
|
+
lock.assertHeld(journalFile);
|
|
163
|
+
// The owner is checked again within removeUpdateGate's short write transaction.
|
|
164
|
+
removeUpdateGate(db, id);
|
|
165
|
+
durableJson(journalFile, {
|
|
166
|
+
...fresh.journal, phase: "released", updatedAt: world.now().toISOString(),
|
|
167
|
+
releasedBeforeSwap: { at: world.now().toISOString(), by: "toolroll release --release-gate", from: fresh.journal.phase, proof: fresh.proof },
|
|
168
|
+
});
|
|
169
|
+
return { ok: true, released: true, id, stage: fresh.stage, proof: fresh.proof };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
db.close();
|
|
173
|
+
}
|
|
141
174
|
}
|
|
142
175
|
finally {
|
|
143
|
-
|
|
176
|
+
lock.release();
|
|
144
177
|
}
|
|
145
|
-
durableJson(join(proved.stage, "deployment.json"), {
|
|
146
|
-
...proved.journal, phase: "released", updatedAt: world.now().toISOString(),
|
|
147
|
-
releasedBeforeSwap: { at: world.now().toISOString(), by: "toolroll release --release-gate", from: proved.journal.phase, proof: proved.proof },
|
|
148
|
-
});
|
|
149
|
-
return { ok: true, released: true, id, stage: proved.stage, proof: proved.proof };
|
|
150
178
|
}
|
package/dist/scope.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
3
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
4
4
|
import { validateScopeText } from "./task-text.js";
|
|
5
|
+
import { contractChangesOf } from "./contract-changes.js";
|
|
5
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
7
|
import { projectAuthority } from "./project-access.js";
|
|
7
8
|
import { activeRemote } from "./remote-run.js";
|
|
@@ -558,6 +559,19 @@ route) {
|
|
|
558
559
|
.slice(0, 32);
|
|
559
560
|
}
|
|
560
561
|
export function propose(store, input) {
|
|
562
|
+
return store.transact(() => {
|
|
563
|
+
const previous = store.getScope(input.taskId);
|
|
564
|
+
const saved = proposeLocked(store, input);
|
|
565
|
+
// A rewritten goal, exclusions, touches or rubric on a task that is planning starts its planning again (v119), in
|
|
566
|
+
// the same transaction: the next planner plans for the new terms, never from the old plan or its answers. An
|
|
567
|
+
// identical refile changes nothing; the planner's own handoff writes its draft without coming through here.
|
|
568
|
+
const ref = store.lookupRef(input.taskId);
|
|
569
|
+
if (ref !== null && ref.plan !== null && (previous === null || contractChangesOf(previous, saved).length > 0))
|
|
570
|
+
store.restartPlanning(ref.id, input.now);
|
|
571
|
+
return saved;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function proposeLocked(store, input) {
|
|
561
575
|
const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null } = input;
|
|
562
576
|
const draft = { goal, outOfScope, touches: [...touches], budgetMicrousd, acceptance: [...acceptance], qualityMode, candidate: input.candidate ?? null };
|
|
563
577
|
const previous = store.getScope(taskId);
|