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 CHANGED
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
19
19
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
20
20
  [![MIT License](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
21
21
 
22
- **Current release: [v0.13.18](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.18)**
23
- · [Release notes](docs/release-v0.13.18.md)
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.18
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.18
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.18**. SWE-bench remains an optional
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.18-direct-settlement-v1`; the unchanged Codex skill marker is
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
@@ -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.18-direct-settlement-v1";
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;
@@ -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.18-direct-settlement-v1";
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)) };
@@ -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
- const localV2Wrapper = /^\s*export\s*\{\s*default\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*$/u.test(wrapperSource);
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 {};