sortie-dogs 0.13.18 → 0.13.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/codex-mission.js +2 -0
- package/dist/core/initialize.js +4 -1
- package/dist/core/operator-mission.d.ts +4 -1
- package/dist/core/operator-mission.js +23 -1
- package/dist/core/operator-runtime.d.ts +4 -0
- package/dist/core/operator-runtime.js +21 -3
- package/dist/core/work-quantity-observations.d.ts +13 -0
- package/dist/core/work-quantity-observations.js +91 -0
- package/dist/core/work-quantity.d.ts +94 -0
- package/dist/core/work-quantity.js +463 -0
- package/dist/plugin/mission-progress.d.ts +1 -1
- package/dist/plugin/mission-progress.js +17 -6
- package/dist/plugin/operator-status-output.js +1 -1
- package/dist/plugin/profiled.js +65 -6
- package/dist/plugin/v2.js +17 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.13.
|
|
23
|
-
· [Release notes](docs/release-v0.13.
|
|
22
|
+
**Current release: [v0.13.19](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.19)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.19.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.13.19
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.13.19
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -125,6 +125,11 @@ Discovery: User → Operator → Coordinator → Worker → Validation → Revi
|
|
|
125
125
|
The default Mission profile is serial. Background responsiveness and status observations do not
|
|
126
126
|
add parallel writers, replay unknown commands or imply completion. More agents are not the goal.
|
|
127
127
|
|
|
128
|
+
Whole-request workload appears as `全体作業量 n/100(概算)`, using ordinary plans and native
|
|
129
|
+
work observations, not budget consumption or fixed stage shares. Calculation adds no model
|
|
130
|
+
request or verification; formal acceptance still owns `100/100`.
|
|
131
|
+
[Workload estimates, optional weights and runtime cost](docs/work-quantity-progress.md).
|
|
132
|
+
|
|
128
133
|
For **run-once / result-collection** requests, a terminal failure can be the requested result:
|
|
129
134
|
validate and report it without an unauthorized repair/rerun loop. Acceptance preserves the real
|
|
130
135
|
operation exit and `execution-failed` outcome; it never turns a failed benchmark into a successful
|
|
@@ -188,7 +193,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
188
193
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
189
194
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
190
195
|
|
|
191
|
-
All scores belong to their fixed candidates, **not v0.13.
|
|
196
|
+
All scores belong to their fixed candidates, **not v0.13.19**. SWE-bench remains an optional
|
|
192
197
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
193
198
|
|
|
194
199
|
## Configuration
|
|
@@ -211,7 +216,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
211
216
|
|
|
212
217
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
213
218
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
214
|
-
The current Mission asset marker is `0.13.
|
|
219
|
+
The current Mission asset marker is `0.13.19-workload-progress-v1`; the unchanged Codex skill marker is
|
|
215
220
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
216
221
|
|
|
217
222
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -227,7 +232,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
227
232
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
228
233
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
229
234
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
230
|
-
- **Releases:** [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
235
|
+
- **Releases:** [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
231
236
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
232
237
|
|
|
233
238
|
## Community
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.19-workload-progress-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.13.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.13.19-workload-progress-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -118,6 +118,8 @@ export function codexMissionProgress(event) {
|
|
|
118
118
|
command: bounded(args.command), workdir: bounded(args.workdir), cwd: bounded(metadata.cwd), reason: bounded(args.reason),
|
|
119
119
|
agent: bounded(args.subagent_type), description: bounded(args.description), child_session_id: bounded(metadata.sessionId),
|
|
120
120
|
outcome: bounded(receipt.status), exit: metadata.exit, executor: metadata.executor,
|
|
121
|
+
...(record(receipt.work_quantity ?? metadata.work_quantity) ? { work_quantity: receipt.work_quantity ?? metadata.work_quantity,
|
|
122
|
+
work_quantity_display: bounded(receipt.work_quantity_display ?? metadata.work_quantity_display) } : {}),
|
|
121
123
|
next_action: bounded(receipt.next_action),
|
|
122
124
|
summary: bounded(args.summary ?? receipt.summary),
|
|
123
125
|
detail: bounded(receipt.reason ?? receipt.error ?? (item.success === false ? text : undefined)) };
|
package/dist/core/initialize.js
CHANGED
|
@@ -391,7 +391,10 @@ async function v010Config(root, global) {
|
|
|
391
391
|
const update = (path, value) => { content = applyEdits(content, modify(content, path, value, options)); };
|
|
392
392
|
const wrapper = `${prefix}plugins/sortie-dogs/index.js`;
|
|
393
393
|
const wrapperSource = await readFile(resolve(root, wrapper), "utf8").catch(() => "");
|
|
394
|
-
|
|
394
|
+
// Both the package re-export and the release fixture's factory wrapper are
|
|
395
|
+
// automatically discovered by V2. Adding the package would load the same ID twice.
|
|
396
|
+
const localV2Wrapper = /^\s*export\s*\{\s*default\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*$/u.test(wrapperSource) ||
|
|
397
|
+
/^\s*import\s*\{\s*createSortieDogsV2Plugin\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*import\s*\{\s*SortieDogsPlugin\s*\}\s*from\s*["']sortie-dogs\/plugin["'];?\s*export\s+default\s+createSortieDogsV2Plugin\s*\(\s*SortieDogsPlugin\s*\);?\s*$/u.test(wrapperSource);
|
|
395
398
|
const registered = plugins?.some(item => {
|
|
396
399
|
const name = typeof item === "string" ? item : item !== null && typeof item === "object" ? item.package : undefined;
|
|
397
400
|
return typeof name === "string" && /^sortie-dogs(?:@[^/]+)?$/u.test(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type OperatorPlan, type OperatorState, type OperatorTask, type OperatorRuntime } from "./operator-runtime.js";
|
|
2
2
|
import { type RuntimeProfile } from "./runtime-profile.js";
|
|
3
3
|
import { type MissionContinuity } from "./mission-context.js";
|
|
4
|
+
import { type WorkQuantityState } from "./work-quantity.js";
|
|
4
5
|
export { missionValidationCommand } from "./mission-validation-command.js";
|
|
5
6
|
export declare const MISSION_REFERENCE = "SORTIE_MISSION_REF ";
|
|
6
7
|
export declare const MISSION_REVIEW_REFERENCE = "SORTIE_MISSION_REVIEW_REF ";
|
|
@@ -256,6 +257,8 @@ export interface OperatorMission {
|
|
|
256
257
|
status: string;
|
|
257
258
|
at: string;
|
|
258
259
|
}[];
|
|
260
|
+
/** Display-only request workload; excluded from controls, prompts and acceptance. */
|
|
261
|
+
workQuantity?: WorkQuantityState;
|
|
259
262
|
submission: {
|
|
260
263
|
status: "ready" | "needs-decision" | "blocked";
|
|
261
264
|
summary: string;
|
|
@@ -387,7 +390,7 @@ export declare function missionPlan(mission: OperatorMission, raw: unknown, proj
|
|
|
387
390
|
/** Compact provenance, not a new acceptance verdict or a substitute for original-request comparison. */
|
|
388
391
|
export declare function missionAcceptanceSummary(mission: OperatorMission, run: OperatorState | undefined, operators: OperatorRuntime, observe?: (validation: readonly string[], child: string | null, notBefore?: number) => Promise<unknown>): Promise<Record<string, unknown>>;
|
|
389
392
|
export declare function missionReviewIndependent(mission: OperatorMission, child: string | undefined): boolean;
|
|
390
|
-
export declare function missionPacket(mission: OperatorMission, run?: OperatorState): Record<string, unknown>;
|
|
393
|
+
export declare function missionPacket(mission: OperatorMission, run?: OperatorState, includeWorkQuantity?: boolean): Record<string, unknown>;
|
|
391
394
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
392
395
|
export declare function missionReviewTask(mission: OperatorMission): OperatorTask;
|
|
393
396
|
/** Optional implementation notes supplement host-observed source/checks; prose is not a coverage gate. */
|
|
@@ -7,6 +7,8 @@ import { profileAgent } from "./runtime-profile.js";
|
|
|
7
7
|
import { normalizeCommand } from "../plugin/gate.js";
|
|
8
8
|
import { missionValidationCommand } from "./mission-validation-command.js";
|
|
9
9
|
import { retainMissionContext, missionContextView } from "./mission-context.js";
|
|
10
|
+
import { planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
11
|
+
import { applyPendingWork, rememberWorkMission, rememberedWorkRun } from "./work-quantity-observations.js";
|
|
10
12
|
export { missionValidationCommand } from "./mission-validation-command.js";
|
|
11
13
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
12
14
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -299,11 +301,22 @@ export class OperatorMissionRuntime {
|
|
|
299
301
|
return matches.length === 1 ? matches[0] : undefined;
|
|
300
302
|
}
|
|
301
303
|
async save(file, value) {
|
|
304
|
+
if (record(value) && value.version === "0.12" && typeof value.root === "string") {
|
|
305
|
+
const mission = value;
|
|
306
|
+
const run = rememberedWorkRun(this.projectRoot, mission.root);
|
|
307
|
+
const current = run?.runID === mission.runID ? run : undefined;
|
|
308
|
+
if (!mission.workQuantity || current && mission.workQuantity.planRunID !== current.runID) {
|
|
309
|
+
mission.workQuantity = planWorkQuantity(mission, current?.units.map(unit => unit.unit), current, mission.workQuantity, undefined, this.projectRoot);
|
|
310
|
+
}
|
|
311
|
+
mission.workQuantity = syncWorkQuantity(applyPendingWork(this.projectRoot, mission.root, mission.workQuantity), mission, current);
|
|
312
|
+
}
|
|
302
313
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
303
314
|
await mkdir(join(this.projectRoot, this.profile.stateDirectory, "missions"), { recursive: true });
|
|
304
315
|
try {
|
|
305
316
|
await writeFile(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
|
|
306
317
|
await rename(temporary, file);
|
|
318
|
+
if (record(value) && value.version === "0.12" && file === this.file(String(value.root)))
|
|
319
|
+
rememberWorkMission(this.projectRoot, value);
|
|
307
320
|
}
|
|
308
321
|
finally {
|
|
309
322
|
await rm(temporary, { force: true });
|
|
@@ -326,6 +339,8 @@ export class OperatorMissionRuntime {
|
|
|
326
339
|
const state = await this.loadMission(root);
|
|
327
340
|
if (state && (state.version !== "0.12" || state.root !== root))
|
|
328
341
|
throw new Error("mission-state-invalid");
|
|
342
|
+
if (state)
|
|
343
|
+
rememberWorkMission(this.projectRoot, state);
|
|
329
344
|
return state;
|
|
330
345
|
}
|
|
331
346
|
/** Serialize host recovery claims across processes; a stale lock never authorizes takeover. */
|
|
@@ -441,6 +456,7 @@ export class OperatorMissionRuntime {
|
|
|
441
456
|
throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
|
|
442
457
|
}
|
|
443
458
|
previous.requirements = requirements.map((text, index) => ({ id: `R${index + 1}`, text }));
|
|
459
|
+
previous.workQuantity = planWorkQuantity(previous, [], undefined, previous.workQuantity);
|
|
444
460
|
// Only an explicit Mission action adopts the latest real turn; chat capture alone never does.
|
|
445
461
|
if (!previous.requests.some(item => item.id === request.id))
|
|
446
462
|
previous.requests.push(request);
|
|
@@ -468,6 +484,10 @@ export class OperatorMissionRuntime {
|
|
|
468
484
|
...((previous === undefined || previous.phase === "cancelled") &&
|
|
469
485
|
(replaceRequirements || previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
|
|
470
486
|
? { supersededRunID: predecessor } : {}) };
|
|
487
|
+
if (previous?.phase === "cancelled" && !replaceRequirements &&
|
|
488
|
+
previous.requirements.every((requirement, index) => state.requirements[index]?.text === requirement.text)) {
|
|
489
|
+
state.workQuantity = planWorkQuantity(state, [], undefined, previous.workQuantity);
|
|
490
|
+
}
|
|
471
491
|
await this.save(this.file(root), state);
|
|
472
492
|
return state;
|
|
473
493
|
});
|
|
@@ -762,7 +782,7 @@ export function missionReviewIndependent(mission, child) {
|
|
|
762
782
|
return child !== undefined && !mission.sourceAuthors?.includes(child) &&
|
|
763
783
|
!(mission.corrections ?? []).some(correction => correction.author === child);
|
|
764
784
|
}
|
|
765
|
-
export function missionPacket(mission, run) {
|
|
785
|
+
export function missionPacket(mission, run, includeWorkQuantity = true) {
|
|
766
786
|
const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
|
|
767
787
|
if (predecessor)
|
|
768
788
|
run = undefined;
|
|
@@ -770,7 +790,9 @@ export function missionPacket(mission, run) {
|
|
|
770
790
|
const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
|
|
771
791
|
const operationStatus = missionExecutionStatus(mission);
|
|
772
792
|
const operationComplete = missionExecutionComplete(mission);
|
|
793
|
+
const workload = syncWorkQuantity(structuredClone(mission.workQuantity ?? planWorkQuantity(mission, run?.runID === mission.runID ? run.units.map(unit => unit.unit) : [], run)), mission, run);
|
|
773
794
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
795
|
+
...(includeWorkQuantity ? { work_quantity: projectWorkQuantity(workload, mission.phase === "completed", mission.phase === "cancelled") } : {}),
|
|
774
796
|
...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
|
|
775
797
|
completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
|
|
776
798
|
note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
|
|
@@ -2,6 +2,7 @@ import { type RuntimeProfile } from "./runtime-profile.js";
|
|
|
2
2
|
import type { GoalEvidence, GoalTerminalReceipt } from "./goal-bound.js";
|
|
3
3
|
import type { SerialDispatchSettlement } from "../plugin/runtime-bridge.js";
|
|
4
4
|
import { type OperatorContractRepairFileIdentity } from "./operator-contract-repair.js";
|
|
5
|
+
import { type WorkQuantityState, type WorkQuantityProgress } from "./work-quantity.js";
|
|
5
6
|
export declare const OPERATOR_LIMITS: Readonly<{
|
|
6
7
|
units: 32;
|
|
7
8
|
planBytes: number;
|
|
@@ -205,6 +206,8 @@ export interface OperatorAcceptanceAnchor {
|
|
|
205
206
|
readonly handoffHash: string;
|
|
206
207
|
}
|
|
207
208
|
export interface OperatorState {
|
|
209
|
+
/** Display metadata only, never part of the immutable execution plan. */
|
|
210
|
+
workQuantity?: WorkQuantityState;
|
|
208
211
|
readonly schema_version: "0.1";
|
|
209
212
|
readonly profile: string;
|
|
210
213
|
readonly rootSessionID: string;
|
|
@@ -254,6 +257,7 @@ export interface OperatorState {
|
|
|
254
257
|
receipt: GoalTerminalReceipt | null;
|
|
255
258
|
}
|
|
256
259
|
export interface OperatorProgress {
|
|
260
|
+
readonly work_quantity?: WorkQuantityProgress;
|
|
257
261
|
readonly profile: string;
|
|
258
262
|
readonly view: "progress";
|
|
259
263
|
readonly run_id: string;
|
|
@@ -15,6 +15,8 @@ import { ensureGitManagedStateExcluded } from "./git-managed-state.js";
|
|
|
15
15
|
import { operatorContractRepairFingerprint } from "./operator-contract-repair.js";
|
|
16
16
|
import { classifyUnitResult } from "./unit-result-classification.js";
|
|
17
17
|
import { SOURCE_REVIEW_PHASES, SOURCE_REVIEW_RISK_TAGS } from "./consultation.js";
|
|
18
|
+
import { planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
19
|
+
import { applyPendingWork, rememberWorkRun } from "./work-quantity-observations.js";
|
|
18
20
|
export const OPERATOR_LIMITS = Object.freeze({ units: 32, planBytes: 256 * 1024, packetBytes: 24 * 1024,
|
|
19
21
|
remediationReserve: 32 });
|
|
20
22
|
export class OperatorContractError extends Error {
|
|
@@ -319,8 +321,10 @@ export class OperatorRuntime {
|
|
|
319
321
|
async read(root) {
|
|
320
322
|
await this.writes.get(root);
|
|
321
323
|
const cached = this.states.get(root);
|
|
322
|
-
if (cached)
|
|
324
|
+
if (cached) {
|
|
325
|
+
rememberWorkRun(this.projectRoot, cached);
|
|
323
326
|
return structuredClone(cached);
|
|
327
|
+
}
|
|
324
328
|
let source;
|
|
325
329
|
try {
|
|
326
330
|
source = await readFile(this.file(root), "utf8");
|
|
@@ -330,9 +334,13 @@ export class OperatorRuntime {
|
|
|
330
334
|
return undefined;
|
|
331
335
|
throw error;
|
|
332
336
|
}
|
|
333
|
-
if (Buffer.byteLength(source) > OPERATOR_LIMITS.planBytes * 4)
|
|
334
|
-
throw new Error("operator-state-too-large");
|
|
335
337
|
const parsed = JSON.parse(source);
|
|
338
|
+
// Display metadata must not turn an otherwise valid execution state into a refusal.
|
|
339
|
+
if (Buffer.byteLength(source) > OPERATOR_LIMITS.planBytes * 4) {
|
|
340
|
+
const { workQuantity: _display, ...execution } = parsed;
|
|
341
|
+
if (Buffer.byteLength(JSON.stringify(execution)) > OPERATOR_LIMITS.planBytes * 4)
|
|
342
|
+
throw new Error("operator-state-too-large");
|
|
343
|
+
}
|
|
336
344
|
const repairGeneration = parsed.repairGeneration === undefined ? 0 : parsed.repairGeneration;
|
|
337
345
|
const state = { ...parsed,
|
|
338
346
|
repairResidualPaths: parsed.repairResidualPaths === undefined ? [] : parsed.repairResidualPaths,
|
|
@@ -382,9 +390,15 @@ export class OperatorRuntime {
|
|
|
382
390
|
throw new Error("operator-state-invalid");
|
|
383
391
|
}
|
|
384
392
|
this.states.set(root, state);
|
|
393
|
+
rememberWorkRun(this.projectRoot, state);
|
|
385
394
|
return structuredClone(state);
|
|
386
395
|
}
|
|
387
396
|
async save(state) {
|
|
397
|
+
if (!state.workQuantity || state.workQuantity.planRunID !== state.runID)
|
|
398
|
+
state.workQuantity = planWorkQuantity({
|
|
399
|
+
id: state.workQuantity?.scopeID ?? `operator:${state.runID}`, requirements: state.acceptance.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "running", submission: null
|
|
400
|
+
}, state.units.map(unit => unit.unit), state, state.workQuantity, undefined, this.projectRoot);
|
|
401
|
+
syncWorkQuantity(applyPendingWork(this.projectRoot, state.rootSessionID, state.workQuantity), {}, state);
|
|
388
402
|
state.sequence++;
|
|
389
403
|
const serialized = JSON.stringify(state);
|
|
390
404
|
const previous = this.writes.get(state.rootSessionID) ?? Promise.resolve();
|
|
@@ -405,6 +419,7 @@ export class OperatorRuntime {
|
|
|
405
419
|
try {
|
|
406
420
|
await next;
|
|
407
421
|
this.states.set(state.rootSessionID, structuredClone(state));
|
|
422
|
+
rememberWorkRun(this.projectRoot, state);
|
|
408
423
|
}
|
|
409
424
|
catch (error) {
|
|
410
425
|
state.sequence--;
|
|
@@ -1139,6 +1154,7 @@ export class OperatorRuntime {
|
|
|
1139
1154
|
? { ...structuredClone(previous.gitLifecycle), committedHead: null, commitProvenance: null }
|
|
1140
1155
|
: await this.createGitLifecycle(plan, remediationHead);
|
|
1141
1156
|
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash: validatedPlanHash,
|
|
1157
|
+
...(parent?.workQuantity ? { workQuantity: structuredClone(parent.workQuantity) } : {}),
|
|
1142
1158
|
acceptance: plan.acceptance, acceptanceProof: plan.acceptance_proof, acceptanceFingerprint, sourceRefs: plan.source_refs, createdAt: new Date().toISOString(),
|
|
1143
1159
|
parentRunID: parent?.runID ?? null, ...(superseding ? { supersededRunID: previous.runID } : {}), priorAcceptedUnits,
|
|
1144
1160
|
remediationParent: remediationTaskID && remediationHead && parent?.gitLifecycle ? {
|
|
@@ -2219,6 +2235,7 @@ export class OperatorRuntime {
|
|
|
2219
2235
|
const authoritativeAction = nextAction !== undefined ? nextAction
|
|
2220
2236
|
: this.continuationNextAction(state);
|
|
2221
2237
|
return { profile: state.profile, view: "progress", run_id: state.runID, stage: state.phase, decision: state.decision,
|
|
2238
|
+
...(state.workQuantity ? { work_quantity: projectWorkQuantity(state.workQuantity, state.phase === "completed", state.phase === "cancelled") } : {}),
|
|
2222
2239
|
current_unit: current === undefined ? null : { id: current.unit.id, title: current.unit.title, status: current.status },
|
|
2223
2240
|
completed_units: state.units.filter(unit => unit.status === "succeeded").length, total_units: state.units.length,
|
|
2224
2241
|
budget_remaining_units: budgetRemainingUnits, next_action: typeof authoritativeAction === "string" ? authoritativeAction : null };
|
|
@@ -2242,6 +2259,7 @@ export class OperatorRuntime {
|
|
|
2242
2259
|
: `call ${this.profile.toolPrefix}cancel_operator, then call ${this.profile.toolPrefix}prepare_operator with an approved replacement plan for the same goal and exact acceptance; start from git_lifecycle.committed_head, keep writes within git_lifecycle.approved_write_union, retain consumed budget, and do not call resume_operator or edit the committed candidate${carriedAction}`;
|
|
2243
2260
|
const awaitingAcceptanceNextAction = `assess the independent-review requirement and obtain review when required, copying review_dispatch_contract.required_prompt_lines into the reviewer Task prompt with real values; if review PASSes (or policy records an allowed skip), call ${this.profile.toolPrefix}complete_operator with this run_id and acceptance_fingerprint; if review has blocking findings wholly inside the existing exact acceptance, the union of git_lifecycle.approved_write_union and git_lifecycle.remediation_reserve, and remaining cumulative budget, do not complete or ask for user approval: call ${this.profile.toolPrefix}cancel_operator with reason review-blocking, then call ${this.profile.toolPrefix}prepare_operator for one same-goal replacement from git_lifecycle.committed_head targeting only those findings; copy this packet's acceptance array verbatim into the replacement plan without paraphrase, deletion, addition, or reordering, preserve accepted-criteria lineage, and run final canonical validation/review; if the replacement is refused for write scope, report replacement_constraints.blocked_write_paths to the user as the exact paths needing approval and stop, then after the user approves resend the same replacement with git_lifecycle.remediation_scope_expansion set to exactly those paths; if acceptance or budget must increase, stop for the user decision`;
|
|
2244
2261
|
const packet = { profile: state.profile, run_id: state.runID, root_session_id: state.rootSessionID, generation: state.generation,
|
|
2262
|
+
...(state.workQuantity ? { work_quantity: projectWorkQuantity(state.workQuantity, state.phase === "completed", state.phase === "cancelled") } : {}),
|
|
2245
2263
|
sequence: state.sequence, status: state.phase, repair_generation: state.repairGeneration,
|
|
2246
2264
|
acceptance: state.acceptance, acceptance_fingerprint: state.acceptanceFingerprint,
|
|
2247
2265
|
next_task_ref: current?.status === "pending" ? (state.units.length > 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Bounded display observations from already delivered native events. No I/O or authority. */
|
|
2
|
+
import type { OperatorMission } from "./operator-mission.js";
|
|
3
|
+
import type { OperatorState } from "./operator-runtime.js";
|
|
4
|
+
import { type WorkQuantityEvent, type WorkQuantityState, type WorkQuantityProgress } from "./work-quantity.js";
|
|
5
|
+
export declare function rememberWorkMission(directory: string, mission: OperatorMission): void;
|
|
6
|
+
export declare function rememberWorkRun(directory: string, run: OperatorState): void;
|
|
7
|
+
export declare function rememberedWorkRun(directory: string, root: string): OperatorState | undefined;
|
|
8
|
+
export declare function workRootForActor(directory: string, actor: string): string | undefined;
|
|
9
|
+
/** Every event is folded at most once per saved projection; repeated edits are not completed work. */
|
|
10
|
+
export declare function observeActorWork(directory: string, actor: string, event: WorkQuantityEvent): string | undefined;
|
|
11
|
+
export declare function applyPendingWork(directory: string, root: string, state: WorkQuantityState): WorkQuantityState;
|
|
12
|
+
export declare function missionWorkState(directory: string, mission: OperatorMission, run?: OperatorState): WorkQuantityState;
|
|
13
|
+
export declare function cachedWorkProgress(directory: string, actor: string): WorkQuantityProgress | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { observeWorkQuantity, planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
2
|
+
const views = new Map();
|
|
3
|
+
const owners = new Map();
|
|
4
|
+
const identity = (directory, root) => `${directory.replaceAll("\\", "/").replace(/\/$/u, "")}\0${root}`;
|
|
5
|
+
function view(directory, root) {
|
|
6
|
+
const id = identity(directory, root);
|
|
7
|
+
let value = views.get(id);
|
|
8
|
+
if (!value) {
|
|
9
|
+
value = { sequence: 0, events: new Map() };
|
|
10
|
+
views.set(id, value);
|
|
11
|
+
}
|
|
12
|
+
owners.set(id, root);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function rememberWorkMission(directory, mission) {
|
|
16
|
+
const value = view(directory, mission.root);
|
|
17
|
+
if (value.mission?.id !== mission.id && value.mission?.workQuantity?.scopeID !== mission.workQuantity?.scopeID)
|
|
18
|
+
value.events.clear();
|
|
19
|
+
value.mission = mission;
|
|
20
|
+
value.sequence = Math.max(value.sequence, mission.workQuantity?.observationSequence ?? 0);
|
|
21
|
+
delete value.progress;
|
|
22
|
+
if (mission.coordinator)
|
|
23
|
+
owners.set(identity(directory, mission.coordinator), mission.root);
|
|
24
|
+
if (mission.review?.child)
|
|
25
|
+
owners.set(identity(directory, mission.review.child), mission.root);
|
|
26
|
+
for (const attempt of mission.attempts ?? [])
|
|
27
|
+
if (attempt.childSessionID)
|
|
28
|
+
owners.set(identity(directory, attempt.childSessionID), mission.root);
|
|
29
|
+
}
|
|
30
|
+
export function rememberWorkRun(directory, run) {
|
|
31
|
+
const value = view(directory, run.rootSessionID);
|
|
32
|
+
value.run = run;
|
|
33
|
+
value.sequence = Math.max(value.sequence, run.workQuantity?.observationSequence ?? 0);
|
|
34
|
+
delete value.progress;
|
|
35
|
+
if (run.operatorSessionID)
|
|
36
|
+
owners.set(identity(directory, run.operatorSessionID), run.rootSessionID);
|
|
37
|
+
for (const unit of run.units)
|
|
38
|
+
if (unit.childSessionID)
|
|
39
|
+
owners.set(identity(directory, unit.childSessionID), run.rootSessionID);
|
|
40
|
+
}
|
|
41
|
+
export function rememberedWorkRun(directory, root) {
|
|
42
|
+
return views.get(identity(directory, root))?.run;
|
|
43
|
+
}
|
|
44
|
+
export function workRootForActor(directory, actor) { return owners.get(identity(directory, actor)); }
|
|
45
|
+
/** Every event is folded at most once per saved projection; repeated edits are not completed work. */
|
|
46
|
+
export function observeActorWork(directory, actor, event) {
|
|
47
|
+
const root = owners.get(identity(directory, actor));
|
|
48
|
+
if (!root)
|
|
49
|
+
return undefined;
|
|
50
|
+
const value = view(directory, root);
|
|
51
|
+
if (value.mission && ["completed", "cancelled"].includes(value.mission.phase) || !value.mission &&
|
|
52
|
+
value.run && ["completed", "cancelled"].includes(value.run.phase))
|
|
53
|
+
return undefined;
|
|
54
|
+
const run = value.run;
|
|
55
|
+
const unit = run?.units.find(unit => unit.status === "running" && unit.childSessionID === actor);
|
|
56
|
+
const observed = { ...event, ...(unit ? { unit: `${run.runID}/${unit.unit.id}` } : {}) };
|
|
57
|
+
const id = JSON.stringify([event.kind, event.paths, event.command, event.commands, event.checkpoints, observed.unit]);
|
|
58
|
+
value.events.delete(id);
|
|
59
|
+
value.events.set(id, { sequence: ++value.sequence, event: observed });
|
|
60
|
+
delete value.progress;
|
|
61
|
+
if (value.events.size > 512)
|
|
62
|
+
value.events.delete(value.events.keys().next().value);
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
export function applyPendingWork(directory, root, state) {
|
|
66
|
+
const value = views.get(identity(directory, root));
|
|
67
|
+
if (!value)
|
|
68
|
+
return state;
|
|
69
|
+
for (const entry of value.events.values())
|
|
70
|
+
if (entry.sequence > (state.observationSequence ?? 0))
|
|
71
|
+
observeWorkQuantity(state, entry.event);
|
|
72
|
+
state.observationSequence = value.sequence;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
export function missionWorkState(directory, mission, run) {
|
|
76
|
+
const state = mission.workQuantity ?? planWorkQuantity(mission, run?.runID === mission.runID ? run.units.map(unit => unit.unit) : [], run, undefined, undefined, directory);
|
|
77
|
+
return syncWorkQuantity(applyPendingWork(directory, mission.root, structuredClone(state)), mission, run);
|
|
78
|
+
}
|
|
79
|
+
export function cachedWorkProgress(directory, actor) {
|
|
80
|
+
const root = owners.get(identity(directory, actor));
|
|
81
|
+
const value = root && views.get(identity(directory, root));
|
|
82
|
+
if (!value)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (value.progress)
|
|
85
|
+
return value.progress;
|
|
86
|
+
if (value.mission)
|
|
87
|
+
return value.progress = projectWorkQuantity(missionWorkState(directory, value.mission, value.run), value.mission.phase === "completed", value.mission.phase === "cancelled");
|
|
88
|
+
if (value.run?.workQuantity)
|
|
89
|
+
return value.progress = projectWorkQuantity(syncWorkQuantity(applyPendingWork(directory, root, structuredClone(value.run.workQuantity)), {}, value.run), value.run.phase === "completed", value.run.phase === "cancelled");
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Display-only request workload. No filesystem, hashing, model, process or network access. */
|
|
2
|
+
import type { OperatorMission } from "./operator-mission.js";
|
|
3
|
+
import type { OperatorState } from "./operator-runtime.js";
|
|
4
|
+
export type WorkKind = "requirement" | "implementation" | "artifact" | "validation" | "operation" | "review" | "delivery" | "acceptance" | "user-acceptance";
|
|
5
|
+
export interface WorkObservation {
|
|
6
|
+
read?: boolean;
|
|
7
|
+
changed?: boolean;
|
|
8
|
+
started?: boolean;
|
|
9
|
+
passed?: boolean;
|
|
10
|
+
failed?: boolean;
|
|
11
|
+
reported?: boolean;
|
|
12
|
+
settled?: string;
|
|
13
|
+
invalidated?: boolean;
|
|
14
|
+
callID?: string;
|
|
15
|
+
changedTargets?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface WorkQuantityItem {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
kind: WorkKind;
|
|
21
|
+
requirementIDs: string[];
|
|
22
|
+
weight: number;
|
|
23
|
+
basis: "declared" | "derived" | "fallback";
|
|
24
|
+
parentID?: string;
|
|
25
|
+
paths: string[];
|
|
26
|
+
commands: string[];
|
|
27
|
+
units: string[];
|
|
28
|
+
observation: WorkObservation;
|
|
29
|
+
fraction: number;
|
|
30
|
+
removed?: boolean;
|
|
31
|
+
children?: string[];
|
|
32
|
+
targetEstimate?: number;
|
|
33
|
+
aliases?: string[];
|
|
34
|
+
commandPattern?: "pr" | "push" | "publish" | "deploy" | "install";
|
|
35
|
+
}
|
|
36
|
+
export interface WorkQuantityState {
|
|
37
|
+
version: 1;
|
|
38
|
+
scopeID: string;
|
|
39
|
+
revision: number;
|
|
40
|
+
revisionReason?: string;
|
|
41
|
+
items: WorkQuantityItem[];
|
|
42
|
+
estimatedTotal: number;
|
|
43
|
+
estimatedDone: number;
|
|
44
|
+
basis: "declared" | "derived" | "fallback" | "mixed";
|
|
45
|
+
pendingCount: number;
|
|
46
|
+
remainingKinds: WorkKind[];
|
|
47
|
+
lastReview?: string;
|
|
48
|
+
observationSequence?: number;
|
|
49
|
+
planRunID?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface WorkQuantityProgress {
|
|
52
|
+
n: number;
|
|
53
|
+
denominator: 100;
|
|
54
|
+
scope_id: string;
|
|
55
|
+
scope: "whole-request";
|
|
56
|
+
estimated: true;
|
|
57
|
+
basis: WorkQuantityState["basis"];
|
|
58
|
+
estimated_done: number;
|
|
59
|
+
estimated_total: number;
|
|
60
|
+
estimate_revision: number;
|
|
61
|
+
revision_reason?: string;
|
|
62
|
+
completed: boolean;
|
|
63
|
+
cancelled: boolean;
|
|
64
|
+
remaining_kinds: WorkKind[];
|
|
65
|
+
}
|
|
66
|
+
export interface WorkQuantityEvent {
|
|
67
|
+
kind: "read" | "change" | "command-start" | "command-end" | "checkpoint" | "user-acceptance";
|
|
68
|
+
paths?: readonly string[];
|
|
69
|
+
command?: string;
|
|
70
|
+
commands?: readonly string[];
|
|
71
|
+
unit?: string;
|
|
72
|
+
callID?: string;
|
|
73
|
+
exit?: number | null;
|
|
74
|
+
successful?: boolean;
|
|
75
|
+
checkpoints?: readonly string[];
|
|
76
|
+
}
|
|
77
|
+
type WorkUnit = {
|
|
78
|
+
id?: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
objective?: string;
|
|
81
|
+
read?: readonly string[];
|
|
82
|
+
write?: readonly string[];
|
|
83
|
+
validation?: readonly string[];
|
|
84
|
+
requirement_ids?: readonly string[];
|
|
85
|
+
acceptance_indices?: readonly number[];
|
|
86
|
+
};
|
|
87
|
+
/** Concrete work before scheduling/coalescence; broad scopes remain unexpanded estimates. */
|
|
88
|
+
export declare function planWorkQuantity(mission: Pick<OperatorMission, "id" | "requirements" | "kind" | "execution" | "review" | "submission" | "phase">, rawUnits?: readonly WorkUnit[], run?: OperatorState, previous?: WorkQuantityState, declared?: unknown, directory?: string): WorkQuantityState;
|
|
89
|
+
export declare function observeWorkQuantity(state: WorkQuantityState, event: WorkQuantityEvent): WorkQuantityState;
|
|
90
|
+
/** Existing recorded outcomes only. No freshness checks, history reads, Git inspection or acceptance actions. */
|
|
91
|
+
export declare function syncWorkQuantity(state: WorkQuantityState, mission: Partial<OperatorMission>, run?: OperatorState): WorkQuantityState;
|
|
92
|
+
export declare function projectWorkQuantity(state: WorkQuantityState, completed?: boolean, cancelled?: boolean): WorkQuantityProgress;
|
|
93
|
+
export declare function formatWorkQuantity(progress: WorkQuantityProgress): string;
|
|
94
|
+
export {};
|