sortie-dogs 0.9.4 → 0.9.5
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 +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +5 -0
- package/dist/core/goal-bound.js +8 -1
- package/dist/core/goal-report.d.ts +18 -0
- package/dist/core/goal-report.js +21 -0
- package/dist/core/run-flight-ledger.d.ts +5 -0
- package/dist/core/run-flight-ledger.js +17 -1
- package/dist/plugin/index.js +78 -25
- package/dist/plugin/run-metrics.d.ts +10 -2
- package/dist/plugin/run-metrics.js +61 -11
- package/dist/plugin/sortie-career.d.ts +55 -0
- package/dist/plugin/sortie-career.js +144 -0
- package/dist/plugin/sortie-debrief.d.ts +54 -0
- package/dist/plugin/sortie-debrief.js +251 -0
- package/dist/runtime-assets.d.ts +18 -12
- package/dist/runtime-assets.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
24
24
|
|
|
25
25
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
26
26
|
|
|
27
|
-
Release: [v0.9.
|
|
27
|
+
Release: [v0.9.5](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.5)
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ValidationOutcome } from "./validation-budget.js";
|
|
2
|
+
import type { GoalReport } from "./goal-report.js";
|
|
2
3
|
export declare const GOAL_BOUND_SCHEMA_VERSION: "0.1";
|
|
3
4
|
export declare const GOAL_BOUND_METADATA_KEY: "sortie-dogs.goal-bound/v1";
|
|
4
5
|
export type GoalDeliveryMode = "planning-only" | "mvp-first" | "repair-first" | "controlled-change";
|
|
@@ -177,6 +178,10 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
177
178
|
readonly kind: "goal.terminal";
|
|
178
179
|
readonly goal_id: string;
|
|
179
180
|
readonly receipt: GoalTerminalReceipt;
|
|
181
|
+
}) | (GoalEventBase & {
|
|
182
|
+
readonly kind: "goal.reported";
|
|
183
|
+
readonly goal_id: string;
|
|
184
|
+
readonly report: GoalReport;
|
|
180
185
|
});
|
|
181
186
|
export interface GoalFlightEventRecord {
|
|
182
187
|
readonly sequence: number;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -111,6 +111,10 @@ export function reduceGoalFlight(records) {
|
|
|
111
111
|
requireState(record.sequence === index + 1 && record.previous_hash === previous && HASH.test(record.event_hash), "invalid", "Goal ledger chain is malformed.");
|
|
112
112
|
previous = record.event_hash;
|
|
113
113
|
const event = record.event;
|
|
114
|
+
// Optional presentation records never participate in execution replay. Writers validate them;
|
|
115
|
+
// older/newer telemetry definitions must not prevent an existing goal from resuming.
|
|
116
|
+
if (event.kind === "goal.reported")
|
|
117
|
+
continue;
|
|
114
118
|
requireState(instant(event.at), "invalid", "Goal event timestamp is invalid.");
|
|
115
119
|
if (event.kind === "goal.accepted") {
|
|
116
120
|
requireState(state.goal_id === null || state.phase === "terminal", "transition", "An active goal already owns this root.");
|
|
@@ -134,13 +138,16 @@ export function reduceGoalFlight(records) {
|
|
|
134
138
|
else if (event.kind === "goal.revised") {
|
|
135
139
|
requireState(state.phase !== "terminal" && event.revision === state.revision + 1 && event.scope_epoch === state.scope_epoch + 1 &&
|
|
136
140
|
HASH.test(event.acceptance_fingerprint) && event.budget.max_units >= state.consumed_units &&
|
|
141
|
+
event.budget.max_units >= state.validation_budget.consumed &&
|
|
137
142
|
validAcceptanceContract(event.acceptance_contract), "transition", "Scope revision is stale or resets consumed budget.");
|
|
138
143
|
state = { ...state, revision: event.revision, scope_epoch: event.scope_epoch,
|
|
139
144
|
acceptance_fingerprint: event.acceptance_fingerprint, latest_user_message_id: event.origin_user_message_id,
|
|
140
145
|
selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
|
|
146
|
+
validation_budget: { ...state.validation_budget,
|
|
147
|
+
limit: state.validation_budget.limit === null ? null : event.budget.max_units },
|
|
141
148
|
acceptance_contract: event.acceptance_contract,
|
|
142
149
|
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
|
|
143
|
-
tickets: [] };
|
|
150
|
+
receipt: null, tickets: [] };
|
|
144
151
|
}
|
|
145
152
|
else if (event.kind === "ticket.issued") {
|
|
146
153
|
requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Optional, bounded telemetry retained with the existing terminal goal ledger, never an authority input. */
|
|
2
|
+
export interface GoalReport {
|
|
3
|
+
readonly definition: "pre-terminal-host-tokens/v1";
|
|
4
|
+
readonly terminal_key: string;
|
|
5
|
+
readonly tokens: number | null;
|
|
6
|
+
readonly models: readonly {
|
|
7
|
+
readonly model: string;
|
|
8
|
+
readonly tokens: number;
|
|
9
|
+
}[] | null;
|
|
10
|
+
readonly first_pass_eligible: boolean;
|
|
11
|
+
readonly traits: readonly ("pack-tactics" | "recovery" | "clean-sweep")[];
|
|
12
|
+
readonly overlap?: {
|
|
13
|
+
readonly definition: "worker-span-union/v1";
|
|
14
|
+
readonly worker_ms: number;
|
|
15
|
+
readonly wall_ms: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function validGoalReport(value: unknown): value is GoalReport;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function validGoalReport(value) {
|
|
2
|
+
if (value === null || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const item = value;
|
|
5
|
+
const amount = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
6
|
+
const duration = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
7
|
+
const overlap = item.overlap;
|
|
8
|
+
return Object.keys(item).every((key) => ["definition", "terminal_key", "tokens", "models", "first_pass_eligible", "traits", "overlap"].includes(key)) &&
|
|
9
|
+
item.definition === "pre-terminal-host-tokens/v1" && /^sha256:[a-f0-9]{64}$/u.test(item.terminal_key) &&
|
|
10
|
+
(item.tokens === null || amount(item.tokens)) && typeof item.first_pass_eligible === "boolean" &&
|
|
11
|
+
Array.isArray(item.traits) && item.traits.length <= 3 && new Set(item.traits).size === item.traits.length &&
|
|
12
|
+
item.traits.every((trait) => ["pack-tactics", "recovery", "clean-sweep"].includes(trait)) &&
|
|
13
|
+
(overlap === undefined || overlap !== null && typeof overlap === "object" &&
|
|
14
|
+
Object.keys(overlap).every((key) => ["definition", "worker_ms", "wall_ms"].includes(key)) && overlap.definition === "worker-span-union/v1" &&
|
|
15
|
+
duration(overlap.worker_ms) && duration(overlap.wall_ms) && overlap.worker_ms >= overlap.wall_ms) &&
|
|
16
|
+
(item.models === null || Array.isArray(item.models) && item.models.length <= 128 &&
|
|
17
|
+
new Set(item.models.map((entry) => entry?.model)).size === item.models.length &&
|
|
18
|
+
item.models.every((entry) => entry !== null && typeof entry === "object" && Object.keys(entry).every((key) => key === "model" || key === "tokens") &&
|
|
19
|
+
typeof entry.model === "string" && entry.model.length > 0 && entry.model.length <= 512 && amount(entry.tokens)) &&
|
|
20
|
+
item.tokens !== null && item.models.reduce((sum, entry) => sum + entry.tokens, 0) === item.tokens);
|
|
21
|
+
}
|
|
@@ -366,6 +366,11 @@ export declare class RunFlightLedger {
|
|
|
366
366
|
static open(filePath: string, evidence: RunFlightEvidenceAccess): Promise<RunFlightLedger>;
|
|
367
367
|
/** Root goal checkpoints share this owner and lock/write discipline without inventing a Git run. */
|
|
368
368
|
static openGoal(filePath: string): Promise<RunFlightLedger>;
|
|
369
|
+
/** Read-only Career inventory: validates one existing goal ledger without opening another store. */
|
|
370
|
+
static readGoalFile(filePath: string): Promise<{
|
|
371
|
+
readonly records: readonly GoalFlightEventRecord[];
|
|
372
|
+
readonly state: GoalFlightState;
|
|
373
|
+
}>;
|
|
369
374
|
read(): Promise<{
|
|
370
375
|
readonly records: readonly RunFlightEventRecord[];
|
|
371
376
|
readonly state: RunFlightState;
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { LUNA_FABRIC_MAX_ACTIVE } from "./luna-fabric-scheduler.js";
|
|
5
5
|
import { normalizeRelativePath } from "./path.js";
|
|
6
6
|
import { CHILD_TERMINAL_EVIDENCE_FIELDS, isChildTerminalIdentity, reconcileChildTerminal, sameChildTerminalIdentity } from "./child-terminal-reconciliation.js";
|
|
7
|
-
import { GOAL_BOUND_SCHEMA_VERSION, GoalBoundError, reduceGoalFlight } from "./goal-bound.js";
|
|
7
|
+
import { GOAL_BOUND_SCHEMA_VERSION, GoalBoundError, reduceGoalFlight, goalFingerprint } from "./goal-bound.js";
|
|
8
8
|
export const RUN_FLIGHT_LEDGER_SCHEMA_VERSION = "0.1";
|
|
9
9
|
export const MAX_RUN_FLIGHT_EVENTS = 2048;
|
|
10
10
|
export const MAX_RUN_FLIGHT_LEDGER_BYTES = 1024 * 1024;
|
|
@@ -657,6 +657,10 @@ export class RunFlightLedger {
|
|
|
657
657
|
await ledger.readGoal();
|
|
658
658
|
return ledger;
|
|
659
659
|
}
|
|
660
|
+
/** Read-only Career inventory: validates one existing goal ledger without opening another store. */
|
|
661
|
+
static async readGoalFile(filePath) {
|
|
662
|
+
return new RunFlightLedger(filePath, undefined, true).readGoal();
|
|
663
|
+
}
|
|
660
664
|
async read() {
|
|
661
665
|
if (this.#goalMode)
|
|
662
666
|
throw new RunFlightLedgerError("invalid", "Goal ledger requires readGoal().");
|
|
@@ -820,6 +824,18 @@ export class RunFlightLedger {
|
|
|
820
824
|
throw new RunFlightLedgerError("conflict", "Ledger lock remained busy.");
|
|
821
825
|
try {
|
|
822
826
|
const records = await this.#readGoalRecords();
|
|
827
|
+
if (event.kind === "goal.reported" && records.some(({ event: stored }) => stored.kind === "goal.reported" &&
|
|
828
|
+
stored.goal_id === event.goal_id && stored.report?.terminal_key === event.report.terminal_key)) {
|
|
829
|
+
return reduceGoalFlight(records);
|
|
830
|
+
}
|
|
831
|
+
if (event.kind === "goal.reported") {
|
|
832
|
+
const { validGoalReport } = await import("./goal-report.js");
|
|
833
|
+
const state = reduceGoalFlight(records);
|
|
834
|
+
if (state.receipt === null || event.goal_id !== state.goal_id || !validGoalReport(event.report) ||
|
|
835
|
+
event.report.terminal_key !== goalFingerprint(state.receipt)) {
|
|
836
|
+
throw new RunFlightLedgerError("invalid", "Report is not bound to the current terminal receipt.");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
823
839
|
const sequence = records.length + 1;
|
|
824
840
|
const previousHash = records.at(-1)?.event_hash ?? null;
|
|
825
841
|
const record = { sequence, previous_hash: previousHash,
|
package/dist/plugin/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import { TerminalRescueRuntime } from "../core/terminal-rescue-runtime.js";
|
|
|
45
45
|
import { OpenCodeTerminalRescueHost } from "./terminal-rescue-host.js";
|
|
46
46
|
import { AdaptiveRemediationRuntime, AdaptiveRunFlightLineage } from "../core/adaptive-remediation-runtime.js";
|
|
47
47
|
import { DEFAULT_ADAPTIVE_REMEDIATION_MODEL, GitAdaptiveRemediationHost, OpenCodeAdaptiveRemediationProvider } from "./adaptive-remediation-host.js";
|
|
48
|
-
import { collectRunMetrics, createSortieResult, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
48
|
+
import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
49
49
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
50
50
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
51
51
|
const ACTIVE_SESSION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -1127,15 +1127,18 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1127
1127
|
const parallelAcceptanceContinuity = new Map();
|
|
1128
1128
|
const goalRootSessions = new Map();
|
|
1129
1129
|
const goalLedgers = new Map();
|
|
1130
|
+
const goalLedgerFiles = new Map();
|
|
1131
|
+
const goalLedgerDirectories = new Set();
|
|
1130
1132
|
const goalReservations = new Map();
|
|
1131
1133
|
const hostGoalExecutions = new Map();
|
|
1134
|
+
const goalValidationDefects = new Set();
|
|
1132
1135
|
const goalDeclarationAuthority = new Map();
|
|
1133
1136
|
const pendingRealGoalTurns = new Map();
|
|
1134
1137
|
const pendingGoalRecoveries = new Map();
|
|
1135
1138
|
const scheduledGoalRecoveries = new Map();
|
|
1136
1139
|
const globalConfig = await readOptionalGlobalConfig();
|
|
1137
1140
|
const sessionOperationMetrics = new Map();
|
|
1138
|
-
function appLogInfo(message, sessionID, extra) {
|
|
1141
|
+
function appLogInfo(message, sessionID, extra, level = "info") {
|
|
1139
1142
|
const app = input.client?.app;
|
|
1140
1143
|
const log = app?.log;
|
|
1141
1144
|
if (log === undefined)
|
|
@@ -1144,7 +1147,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1144
1147
|
const result = log.call(app, {
|
|
1145
1148
|
body: {
|
|
1146
1149
|
service: "sortie-dogs",
|
|
1147
|
-
level
|
|
1150
|
+
level,
|
|
1148
1151
|
message,
|
|
1149
1152
|
extra: { sessionID: sessionID.slice(0, 128), ...extra },
|
|
1150
1153
|
},
|
|
@@ -1242,15 +1245,22 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1242
1245
|
const key = createHash("sha256").update(root).digest("hex");
|
|
1243
1246
|
const projectRoot = resolve(input.worktree ?? input.directory);
|
|
1244
1247
|
const legacyPath = join(projectRoot, ".sortie-dogs", "run-flight", `${key}.json`);
|
|
1248
|
+
goalLedgerDirectories.add(dirname(legacyPath));
|
|
1245
1249
|
const opened = (async () => {
|
|
1250
|
+
const legacyExists = await stat(legacyPath).then((value) => value.isFile()).catch(() => false);
|
|
1251
|
+
const leaseRoot = legacyExists ? await durableScopeRoot(projectRoot).catch(() => undefined) : await durableScopeRoot(projectRoot);
|
|
1252
|
+
if (leaseRoot !== undefined)
|
|
1253
|
+
goalLedgerDirectories.add(join(dirname(leaseRoot), "run-flight"));
|
|
1246
1254
|
// Existing roots remain on their original owner so an in-flight pre-upgrade goal is not forked.
|
|
1247
|
-
if (
|
|
1255
|
+
if (legacyExists) {
|
|
1256
|
+
goalLedgerFiles.set(root, legacyPath);
|
|
1248
1257
|
return await RunFlightLedger.openGoal(legacyPath);
|
|
1249
1258
|
}
|
|
1250
|
-
const leaseRoot = await durableScopeRoot(projectRoot);
|
|
1251
1259
|
const filePath = leaseRoot === undefined
|
|
1252
1260
|
? legacyPath
|
|
1253
1261
|
: join(dirname(leaseRoot), "run-flight", `${key}.json`);
|
|
1262
|
+
goalLedgerFiles.set(root, filePath);
|
|
1263
|
+
goalLedgerDirectories.add(dirname(filePath));
|
|
1254
1264
|
return await RunFlightLedger.openGoal(filePath);
|
|
1255
1265
|
})();
|
|
1256
1266
|
goalLedgers.set(root, opened);
|
|
@@ -1362,8 +1372,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1362
1372
|
const unitID = authorization.taskID;
|
|
1363
1373
|
const unitBound = unitID !== undefined && goal.outstanding_reservations.some((reservation) => reservation.unit_id === unitID && reservation.session_id === identity.parentID);
|
|
1364
1374
|
const criteria = goal.acceptance_contract?.criteria.filter((criterion) => criterion.validation_command === rawCommand && criterion.expected_outcome === "pass") ?? [];
|
|
1375
|
+
const denyValidation = (reason) => {
|
|
1376
|
+
goalValidationDefects.add(toolInput.sessionID);
|
|
1377
|
+
return new Error(`SORTIE_VALIDATION_BUDGET_DENIED: ${reason}`);
|
|
1378
|
+
};
|
|
1365
1379
|
if (!unitBound)
|
|
1366
|
-
throw
|
|
1380
|
+
throw denyValidation("requirement-unbound");
|
|
1367
1381
|
// Exact generation and formatting checks may support acceptance without proving a criterion.
|
|
1368
1382
|
if (criteria.length === 0)
|
|
1369
1383
|
return;
|
|
@@ -1377,10 +1391,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1377
1391
|
reservation = await ledger.reserveValidation(request, goal.budget?.max_units ?? 1);
|
|
1378
1392
|
}
|
|
1379
1393
|
catch (error) {
|
|
1380
|
-
throw
|
|
1394
|
+
throw denyValidation(`authority-unavailable:${error instanceof Error ? error.name : "unknown"}`);
|
|
1381
1395
|
}
|
|
1382
1396
|
if (reservation.decision !== "ALLOW" || reservation.reservation_id === null) {
|
|
1383
|
-
throw
|
|
1397
|
+
throw denyValidation(reservation.reason);
|
|
1384
1398
|
}
|
|
1385
1399
|
validation = { ledger, request, reservation: reservation.reservation_id };
|
|
1386
1400
|
}
|
|
@@ -1615,11 +1629,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1615
1629
|
: "external_dependency";
|
|
1616
1630
|
receipt = await terminalGoal(sessionID, reason, "stopped").catch(() => undefined);
|
|
1617
1631
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1632
|
+
const snapshot = receipt === undefined ? undefined : await goalLedger(sessionID).then((ledger) => ledger.readGoal()).catch(() => undefined);
|
|
1633
|
+
if (snapshot !== undefined)
|
|
1634
|
+
goal = snapshot.state;
|
|
1620
1635
|
if (receipt !== undefined)
|
|
1621
1636
|
rootAcceptanceContinuity.delete(sessionID);
|
|
1622
|
-
return { outcome, goal, receipt, delivery };
|
|
1637
|
+
return { outcome, goal, receipt, delivery, records: snapshot?.records };
|
|
1623
1638
|
}
|
|
1624
1639
|
function goalDeclarationContract(prompt) {
|
|
1625
1640
|
const lines = prompt.split(/\r?\n/u);
|
|
@@ -1768,27 +1783,29 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1768
1783
|
throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: validated.defects });
|
|
1769
1784
|
}
|
|
1770
1785
|
const declaration = validated.declaration;
|
|
1786
|
+
const units = Number(handoffValue(entries, ["goal_budget_units"]));
|
|
1787
|
+
const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
|
|
1788
|
+
? units : state.budget?.max_units ?? 32;
|
|
1789
|
+
const declaredTime = Number(handoffValue(entries, ["goal_budget_time_ms"]));
|
|
1790
|
+
const declaredCost = Number(handoffValue(entries, ["goal_budget_cost_usd"]));
|
|
1791
|
+
const timeBudget = Number.isFinite(declaredTime) && declaredTime > 0 ? declaredTime : state.budget?.time_ms ?? null;
|
|
1792
|
+
const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
|
|
1793
|
+
const budgetChanged = maxUnits !== state.budget?.max_units || timeBudget !== state.budget?.time_ms ||
|
|
1794
|
+
costBudget !== state.budget?.cost_usd;
|
|
1771
1795
|
const authority = goalDeclarationAuthority.get(sessionID);
|
|
1772
1796
|
if (authority === undefined || authority !== state.latest_user_message_id) {
|
|
1773
|
-
if (declaration.fingerprint !== state.acceptance_fingerprint) {
|
|
1797
|
+
if (declaration.fingerprint !== state.acceptance_fingerprint || budgetChanged) {
|
|
1774
1798
|
throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: [
|
|
1775
1799
|
contractDefect("contract", "/goal_acceptance_fingerprint", "goal_revision_unauthorized"),
|
|
1776
1800
|
] });
|
|
1777
1801
|
}
|
|
1778
1802
|
return state;
|
|
1779
1803
|
}
|
|
1780
|
-
if (declaration.fingerprint === state.acceptance_fingerprint &&
|
|
1804
|
+
if (!budgetChanged && declaration.fingerprint === state.acceptance_fingerprint &&
|
|
1781
1805
|
goalFingerprint(declaration.contract) === goalFingerprint(state.acceptance_contract)) {
|
|
1782
1806
|
goalDeclarationAuthority.delete(sessionID);
|
|
1783
1807
|
return state;
|
|
1784
1808
|
}
|
|
1785
|
-
const units = Number(handoffValue(entries, ["goal_budget_units"]));
|
|
1786
|
-
const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
|
|
1787
|
-
? units : state.budget?.max_units ?? 32;
|
|
1788
|
-
const declaredTime = Number(handoffValue(entries, ["goal_budget_time_ms"]));
|
|
1789
|
-
const declaredCost = Number(handoffValue(entries, ["goal_budget_cost_usd"]));
|
|
1790
|
-
const timeBudget = Number.isFinite(declaredTime) && declaredTime > 0 ? declaredTime : state.budget?.time_ms ?? null;
|
|
1791
|
-
const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
|
|
1792
1809
|
state = await ledger.appendGoal({ kind: "goal.revised", at: new Date().toISOString(), goal_id: state.goal_id,
|
|
1793
1810
|
revision: state.revision + 1, scope_epoch: state.scope_epoch + 1,
|
|
1794
1811
|
acceptance_fingerprint: declaration.fingerprint, origin_user_message_id: state.latest_user_message_id,
|
|
@@ -1904,7 +1921,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1904
1921
|
output.status === "cancel" || output.status === "cancelled";
|
|
1905
1922
|
const hostBindingDefect = childSessionID !== undefined && [...(bindingDenials.get(reservation.root)?.values() ?? [])]
|
|
1906
1923
|
.some((candidateDenials) => [...candidateDenials.values()].includes(childSessionID));
|
|
1907
|
-
const
|
|
1924
|
+
const failedAcceptanceExecution = [...hostGoalExecutions.values()].some((execution) => execution.root === reservation.root && execution.sessionID === childSessionID &&
|
|
1925
|
+
execution.endedAt !== undefined && Date.parse(execution.startedAt) >= reservation.started - 1000 &&
|
|
1926
|
+
execution.outcome === "fail");
|
|
1927
|
+
const processDefect = !failedAcceptanceExecution && (childSessionID === undefined || hostBindingDefect ||
|
|
1928
|
+
goalValidationDefects.has(childSessionID));
|
|
1908
1929
|
const resultClass = progress ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
|
|
1909
1930
|
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
|
|
1910
1931
|
reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
|
|
@@ -1913,6 +1934,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1913
1934
|
progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
|
|
1914
1935
|
evidence: acceptedEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started), cost_usd: null });
|
|
1915
1936
|
if (childSessionID !== undefined) {
|
|
1937
|
+
goalValidationDefects.delete(childSessionID);
|
|
1916
1938
|
for (const [executionCallID, execution] of hostGoalExecutions) {
|
|
1917
1939
|
if (execution.root === reservation.root && execution.sessionID === childSessionID)
|
|
1918
1940
|
hostGoalExecutions.delete(executionCallID);
|
|
@@ -4118,7 +4140,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4118
4140
|
const remedies = {
|
|
4119
4141
|
"session-inactive": {
|
|
4120
4142
|
recoverable: true,
|
|
4121
|
-
remedy: "Freshly redispatch
|
|
4143
|
+
remedy: "Freshly redispatch through dog-coordinator's admitted dog-worker Task, with prompt text containing role, project_root, source_manifest or operation_manifest, and acceptance or validation fields. A standalone build/fixer Task is not a registered Sortie worker; copying these fields, a bare resume, or a file read cannot activate it. Preserve explicit agent selection and resolve any existing goal stop before using the Sortie workflow; do not repeat the same standalone redispatch or reset its ledger to bypass a stop.",
|
|
4122
4144
|
},
|
|
4123
4145
|
"session-expired": {
|
|
4124
4146
|
recoverable: true,
|
|
@@ -4659,6 +4681,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4659
4681
|
clearSessionLinks(sessionID);
|
|
4660
4682
|
}
|
|
4661
4683
|
function evictSession(sessionID) {
|
|
4684
|
+
goalValidationDefects.delete(sessionID);
|
|
4662
4685
|
activeSessions.delete(sessionID);
|
|
4663
4686
|
sessionOperationMetrics.delete(sessionID);
|
|
4664
4687
|
rootAcceptanceContinuity.delete(sessionID);
|
|
@@ -5354,13 +5377,33 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5354
5377
|
startedAt: terminal.receipt.started_at,
|
|
5355
5378
|
endedAt: terminal.receipt.ended_at,
|
|
5356
5379
|
}).catch(() => undefined));
|
|
5357
|
-
|
|
5380
|
+
let sortieResult = terminal?.receipt === undefined || terminal.goal === undefined
|
|
5358
5381
|
? undefined
|
|
5359
|
-
: createSortieResult(terminal.receipt, terminal.goal, metrics, new Date().toISOString());
|
|
5382
|
+
: createSortieResult(terminal.receipt, terminal.goal, metrics, new Date().toISOString(), terminal.records);
|
|
5383
|
+
if (sortieResult !== undefined && terminal?.receipt !== undefined && terminal.records !== undefined) {
|
|
5384
|
+
try {
|
|
5385
|
+
const ledger = await goalLedger(textInput.sessionID);
|
|
5386
|
+
const report = createGoalReport(sortieResult, terminal.receipt);
|
|
5387
|
+
let records = terminal.records;
|
|
5388
|
+
if (!records.some(({ event }) => event.kind === "goal.reported" && event.report?.terminal_key === report.terminal_key)) {
|
|
5389
|
+
await ledger.appendGoal({ kind: "goal.reported", at: new Date().toISOString(), goal_id: terminal.receipt.goal_id, report });
|
|
5390
|
+
records = (await ledger.readGoal()).records;
|
|
5391
|
+
}
|
|
5392
|
+
const currentPath = goalLedgerFiles.get(goalRoot(textInput.sessionID));
|
|
5393
|
+
if (currentPath !== undefined) {
|
|
5394
|
+
const { collectCareer } = await import("./sortie-career.js");
|
|
5395
|
+
sortieResult = { ...sortieResult, career: await collectCareer([...goalLedgerDirectories], currentPath, records, (path) => RunFlightLedger.readGoalFile(path)) };
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
catch {
|
|
5399
|
+
appLogInfo("run-metrics.career-unavailable", textInput.sessionID, { outcome: runOutcome }, "warn");
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5360
5402
|
if (sortieResult !== undefined)
|
|
5361
5403
|
textOutput.text = insertSortieResult(textOutput.text, sortieResult);
|
|
5362
5404
|
else if (metrics !== undefined && runOutcome === "DONE")
|
|
5363
5405
|
textOutput.text = insertRunMetrics(textOutput.text, metrics);
|
|
5406
|
+
const { debrief: _debriefObservation, ...metricSummary } = metrics ?? {};
|
|
5364
5407
|
appLogInfo("run-metrics.snapshot", textInput.sessionID, {
|
|
5365
5408
|
available: metrics !== undefined,
|
|
5366
5409
|
outcome: runOutcome,
|
|
@@ -5371,7 +5414,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5371
5414
|
resultProof: sortieResult.proof.overall,
|
|
5372
5415
|
accountingPhase: sortieResult.accounting_phase,
|
|
5373
5416
|
}),
|
|
5374
|
-
...
|
|
5417
|
+
...metricSummary,
|
|
5375
5418
|
...operationMetricsSnapshot(textInput.sessionID),
|
|
5376
5419
|
});
|
|
5377
5420
|
}
|
|
@@ -5705,6 +5748,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5705
5748
|
*/
|
|
5706
5749
|
"tool.execute.after": async (toolInput, output) => {
|
|
5707
5750
|
await recordHostGoalEnd(toolInput, output);
|
|
5751
|
+
// A completed host question is a new user-interaction boundary, just like chat input.
|
|
5752
|
+
// It authorizes one subsequent typed declaration; it does not itself grant budget or clear a stop.
|
|
5753
|
+
if (toolInput.tool === "question" && toolInput.sessionID !== undefined &&
|
|
5754
|
+
isCoordinatorSession(toolInput.sessionID) && typeof output.output === "string" &&
|
|
5755
|
+
output.output.trim().length > 0 && output.status !== "error" && output.status !== "cancelled") {
|
|
5756
|
+
const goal = await currentGoal(toolInput.sessionID);
|
|
5757
|
+
if (goal.phase !== "terminal" && goal.latest_user_message_id !== null) {
|
|
5758
|
+
goalDeclarationAuthority.set(toolInput.sessionID, goal.latest_user_message_id);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5708
5761
|
diagnosisChildren.get(toolInput.sessionID ?? "")?.tools.delete(toolInput.callID ?? "");
|
|
5709
5762
|
const diagnosisCandidate = toolInput.tool === "task" ? diagnosisCalls.get(toolInput.callID ?? "") : undefined;
|
|
5710
5763
|
const diagnosis = diagnosisCandidate?.context.ownerRoot === toolInput.sessionID ? diagnosisCandidate : undefined;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { GoalFlightState, GoalTerminalReceipt } from "../core/goal-bound.js";
|
|
1
|
+
import type { GoalFlightState, GoalTerminalReceipt, GoalFlightEventRecord } from "../core/goal-bound.js";
|
|
2
|
+
import { type Debrief, type DebriefObservation } from "./sortie-debrief.ts";
|
|
3
|
+
import type { GoalReport } from "../core/goal-report.ts";
|
|
4
|
+
import { type SortieCareer } from "./sortie-career.ts";
|
|
2
5
|
export interface RunMetricsClient {
|
|
3
6
|
readonly session?: {
|
|
4
7
|
readonly get?: (request: {
|
|
@@ -44,6 +47,8 @@ export interface RunMetrics {
|
|
|
44
47
|
readonly sessions: number | undefined;
|
|
45
48
|
readonly cacheRatio: number | undefined;
|
|
46
49
|
readonly roles: Readonly<Record<string, RunRoleMetrics>> | undefined;
|
|
50
|
+
/** Optional for compatibility with older host snapshots. Collected in the existing history pass. */
|
|
51
|
+
readonly debrief?: DebriefObservation;
|
|
47
52
|
}
|
|
48
53
|
export interface RunRoleMetrics {
|
|
49
54
|
readonly tokens: number;
|
|
@@ -72,6 +77,8 @@ export interface SortieResult {
|
|
|
72
77
|
readonly result_id: readonly [goalID: string, terminalRevision: number];
|
|
73
78
|
readonly accounting_phase: "pre-terminal";
|
|
74
79
|
readonly as_of: string;
|
|
80
|
+
readonly debrief?: Debrief;
|
|
81
|
+
readonly career?: SortieCareer;
|
|
75
82
|
readonly mission: {
|
|
76
83
|
readonly status: "COMPLETED" | "INTERRUPTED" | "EXTERNAL_BLOCKER" | "USER_DECISION";
|
|
77
84
|
readonly stop_reason: GoalTerminalReceipt["stop_reason"];
|
|
@@ -102,7 +109,7 @@ export interface SortieResult {
|
|
|
102
109
|
}
|
|
103
110
|
type SortieGoalSnapshot = Pick<GoalFlightState, "acceptance_contract" | "consumed_time_ms" | "satisfied_criteria">;
|
|
104
111
|
/** Builds a pure terminal snapshot from the durable goal receipt and already-observed host metrics. */
|
|
105
|
-
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string): SortieResult;
|
|
112
|
+
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string, records?: readonly GoalFlightEventRecord[]): SortieResult;
|
|
106
113
|
export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
|
|
107
114
|
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
|
|
108
115
|
export declare function formatSortieResult(result: SortieResult): string;
|
|
@@ -114,4 +121,5 @@ export declare function replaceDoneTerminalStatus(text: string, replacement: str
|
|
|
114
121
|
export declare function sanitizeTerminalReport(text: string): string;
|
|
115
122
|
export declare function insertRunMetrics(text: string, metrics: RunMetrics): string;
|
|
116
123
|
export declare function insertSortieResult(text: string, result: SortieResult): string;
|
|
124
|
+
export declare function createGoalReport(result: SortieResult, receipt: GoalTerminalReceipt): GoalReport;
|
|
117
125
|
export {};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { buildDebrief, renderDebrief, observeDebriefSession } from "./sortie-debrief.js";
|
|
2
|
+
import { goalFingerprint } from "../core/goal-bound.js";
|
|
3
|
+
import { renderCareer } from "./sortie-career.js";
|
|
1
4
|
const unavailable = (reason) => ({ availability: "unavailable", value: null, reason });
|
|
2
5
|
const available = (value, provenance) => ({ availability: "available", value, provenance });
|
|
3
6
|
function elapsedBetween(start, end) {
|
|
@@ -6,7 +9,7 @@ function elapsedBetween(start, end) {
|
|
|
6
9
|
return Number.isFinite(started) && Number.isFinite(ended) && ended >= started ? ended - started : undefined;
|
|
7
10
|
}
|
|
8
11
|
/** Builds a pure terminal snapshot from the durable goal receipt and already-observed host metrics. */
|
|
9
|
-
export function createSortieResult(receipt, goal, metrics, asOf = receipt.ended_at) {
|
|
12
|
+
export function createSortieResult(receipt, goal, metrics, asOf = receipt.ended_at, records) {
|
|
10
13
|
const goalWall = elapsedBetween(receipt.started_at, receipt.ended_at);
|
|
11
14
|
const firstVerifiable = receipt.milestone_at === null
|
|
12
15
|
? undefined
|
|
@@ -25,11 +28,13 @@ export function createSortieResult(receipt, goal, metrics, asOf = receipt.ended_
|
|
|
25
28
|
: receipt.stop_reason === "external_dependency" || receipt.stop_reason === "persistence_unavailable"
|
|
26
29
|
? "EXTERNAL_BLOCKER"
|
|
27
30
|
: "INTERRUPTED";
|
|
31
|
+
const debrief = buildDebrief(receipt, goal.acceptance_contract, metrics?.debrief, records);
|
|
28
32
|
return {
|
|
29
33
|
schema_version: "0.1",
|
|
30
34
|
result_id: [receipt.goal_id, receipt.terminal_revision],
|
|
31
35
|
accounting_phase: "pre-terminal",
|
|
32
36
|
as_of: asOf,
|
|
37
|
+
debrief,
|
|
33
38
|
mission: {
|
|
34
39
|
status: missionStatus,
|
|
35
40
|
stop_reason: receipt.stop_reason,
|
|
@@ -39,7 +44,9 @@ export function createSortieResult(receipt, goal, metrics, asOf = receipt.ended_
|
|
|
39
44
|
worker_execution_ms: goal.consumed_time_ms === null
|
|
40
45
|
? unavailable("goal-usage-unavailable")
|
|
41
46
|
: available(goal.consumed_time_ms, "goal-ledger"),
|
|
42
|
-
execution_compression:
|
|
47
|
+
execution_compression: debrief.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
48
|
+
? available(debrief.overlap.workerMilliseconds / debrief.overlap.wallMilliseconds, "host-reported")
|
|
49
|
+
: unavailable("worker-overlap-unavailable"),
|
|
43
50
|
first_verifiable_ms: firstVerifiable === undefined
|
|
44
51
|
? unavailable("milestone-unavailable")
|
|
45
52
|
: available(firstVerifiable, "goal-receipt"),
|
|
@@ -194,11 +201,14 @@ export async function collectRunMetrics(client, rootSessionID, directory, now =
|
|
|
194
201
|
let cost = 0;
|
|
195
202
|
let costAvailable = true;
|
|
196
203
|
const roleMetrics = new Map();
|
|
204
|
+
const observedSessions = [];
|
|
197
205
|
for (const id of ids) {
|
|
198
206
|
try {
|
|
199
207
|
const messages = assistantMessages(await session.messages.call(session, { path: { id }, query: { directory } }));
|
|
200
208
|
if (messages === undefined)
|
|
201
209
|
return undefined;
|
|
210
|
+
const observed = observeDebriefSession(id, id === rootSessionID, messages, window === undefined ? undefined : { start: windowStart, end: windowEnd });
|
|
211
|
+
observedSessions.push(observed);
|
|
202
212
|
for (const message of messages) {
|
|
203
213
|
const info = record(message.info) ?? message;
|
|
204
214
|
const time = record(info.time) ?? record(message.time);
|
|
@@ -240,6 +250,12 @@ export async function collectRunMetrics(client, rootSessionID, directory, now =
|
|
|
240
250
|
roleMetrics.set(agent, role);
|
|
241
251
|
for (const usage of fresh) {
|
|
242
252
|
const tokens = messageTokens(usage.value);
|
|
253
|
+
const modelInfo = record(usage.value.info) ?? usage.value;
|
|
254
|
+
const provider = modelInfo.providerID ?? info.providerID;
|
|
255
|
+
const model = modelInfo.modelID ?? info.modelID;
|
|
256
|
+
const modelKey = typeof provider === "string" && typeof model === "string" ? `${provider}/${model}` : "未分類";
|
|
257
|
+
observed.models[modelKey] = tokens === undefined || observed.models[modelKey] === null ? null
|
|
258
|
+
: (observed.models[modelKey] ?? 0) + tokens.total;
|
|
243
259
|
if (tokens !== undefined) {
|
|
244
260
|
totalTokens += tokens.total;
|
|
245
261
|
inputTokens += tokens.input;
|
|
@@ -309,6 +325,8 @@ export async function collectRunMetrics(client, rootSessionID, directory, now =
|
|
|
309
325
|
cacheRatio: role.tokens > 0 ? role.cacheReadTokens / role.tokens : undefined,
|
|
310
326
|
}]))
|
|
311
327
|
: undefined,
|
|
328
|
+
debrief: { complete: hierarchyComplete && messagesComplete, sessions: observedSessions,
|
|
329
|
+
window: window === undefined ? undefined : { start: windowStart, end: windowEnd } },
|
|
312
330
|
};
|
|
313
331
|
}
|
|
314
332
|
function duration(milliseconds) {
|
|
@@ -331,11 +349,14 @@ export function formatSortieResult(result) {
|
|
|
331
349
|
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了"
|
|
332
350
|
: "ユーザー判断待ち(未完了)";
|
|
333
351
|
return [
|
|
334
|
-
"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
"**🐾 SORTIE DOGS — 帰還報告**",
|
|
353
|
+
`**⚡ 時間:** ${metricText(result.speed.goal_wall_ms, duration)}`,
|
|
354
|
+
`**🪙 使用量:** ${metricText(result.cost.total_tokens, (value) => `${value.toLocaleString("ja-JP")} tokens`)} · host推定額 ${metricText(result.cost.cost_usd, (value) => `$${value.toFixed(4)}`)}(実課金換算なし)`,
|
|
355
|
+
...renderDebrief(result.debrief),
|
|
356
|
+
`**🛡 達成:** ${achievement} · 達成条件 ${criteria}`,
|
|
357
|
+
"*最終応答生成前の計測*",
|
|
358
|
+
...renderCareer(result.career),
|
|
359
|
+
].join("\n\n");
|
|
339
360
|
}
|
|
340
361
|
export function formatRunMetrics(metrics) {
|
|
341
362
|
const elapsed = metrics.durationMilliseconds === undefined ? "duration unavailable" : `${duration(metrics.durationMilliseconds)} wall-clock`;
|
|
@@ -441,10 +462,39 @@ export function insertSortieResult(text, result) {
|
|
|
441
462
|
const checkpoint = terminalCheckpoint(visible);
|
|
442
463
|
if (checkpoint === undefined)
|
|
443
464
|
return visible;
|
|
444
|
-
|
|
445
|
-
return visible;
|
|
465
|
+
// A title in model text is not trusted evidence. Replace existing cards, including legacy cards.
|
|
446
466
|
const newline = visible.includes("\r\n") ? "\r\n" : "\n";
|
|
447
467
|
const lines = visible.split(/\r?\n/u);
|
|
448
|
-
|
|
449
|
-
|
|
468
|
+
const cardLines = new Set(topLevelLines(visible).filter(({ index, line }) => index > checkpoint.index &&
|
|
469
|
+
/^(?:\*\*(?:Sortie Result|🐾 SORTIE DOGS — 帰還報告|📜 PACK RECORD|↳\*\*|(?:Speed|Cost|達成|⚡ 時間|🪙 使用量|🐕 出撃隊|モデル別token内訳|実行重複率|🛡 達成|確認|🏅 今回の戦績|戦績|初回完遂|累積使用量|累積モデル|累積時間|累積実行重複率|保存範囲|🎖 隊の称号):)|\*最終応答生成前の計測\*)/u.test(line)).map(({ index }) => index));
|
|
470
|
+
const cleaned = lines.filter((_, index) => !cardLines.has(index));
|
|
471
|
+
while (cleaned[checkpoint.index + 1] === "")
|
|
472
|
+
cleaned.splice(checkpoint.index + 1, 1);
|
|
473
|
+
let card;
|
|
474
|
+
try {
|
|
475
|
+
card = formatSortieResult(result);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
card = "**🐾 SORTIE DOGS — 帰還報告**\n**確認:** 表示集計を取得できません。任務結果は先頭の状態を参照。";
|
|
479
|
+
}
|
|
480
|
+
cleaned.splice(checkpoint.index + 1, 0, "", card, "");
|
|
481
|
+
return cleaned.join(newline).trimEnd();
|
|
482
|
+
}
|
|
483
|
+
export function createGoalReport(result, receipt) {
|
|
484
|
+
const tokens = result.cost.total_tokens.availability === "available" && Number.isSafeInteger(result.cost.total_tokens.value)
|
|
485
|
+
? result.cost.total_tokens.value : null;
|
|
486
|
+
const mix = result.debrief?.mix;
|
|
487
|
+
const traits = [];
|
|
488
|
+
if (result.debrief?.traits.includes("連携作戦"))
|
|
489
|
+
traits.push("pack-tactics");
|
|
490
|
+
if (result.debrief?.traits.includes("修正から復帰"))
|
|
491
|
+
traits.push("recovery");
|
|
492
|
+
if (result.debrief?.traits.includes("一発完遂"))
|
|
493
|
+
traits.push("clean-sweep");
|
|
494
|
+
return { definition: "pre-terminal-host-tokens/v1", terminal_key: goalFingerprint(receipt), tokens,
|
|
495
|
+
models: mix != null && mix.length <= 128 && mix.every((entry) => entry.model.length <= 512) &&
|
|
496
|
+
mix.reduce((sum, entry) => sum + entry.tokens, 0) === tokens ? mix.map(({ model, tokens }) => ({ model, tokens })) : null,
|
|
497
|
+
first_pass_eligible: result.debrief?.firstPassEligible === true, traits,
|
|
498
|
+
...(result.debrief?.overlap === undefined ? {} : { overlap: { definition: "worker-span-union/v1",
|
|
499
|
+
worker_ms: result.debrief.overlap.workerMilliseconds, wall_ms: result.debrief.overlap.wallMilliseconds } }) };
|
|
450
500
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type GoalFlightEventRecord } from "../core/goal-bound.ts";
|
|
2
|
+
export interface CareerCoverage {
|
|
3
|
+
readonly files: number;
|
|
4
|
+
readonly included: number;
|
|
5
|
+
readonly unavailable: number;
|
|
6
|
+
readonly truncated: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SortieCareer {
|
|
9
|
+
readonly scope: "retained-project-goals";
|
|
10
|
+
readonly since: string | null;
|
|
11
|
+
readonly coverage: CareerCoverage;
|
|
12
|
+
readonly goals: number;
|
|
13
|
+
readonly completed: number;
|
|
14
|
+
readonly interrupted: number;
|
|
15
|
+
readonly external: number;
|
|
16
|
+
readonly decision: number;
|
|
17
|
+
readonly active: number;
|
|
18
|
+
readonly telemetryCovered: number;
|
|
19
|
+
readonly firstPass: {
|
|
20
|
+
readonly count: number;
|
|
21
|
+
readonly eligible: number;
|
|
22
|
+
};
|
|
23
|
+
readonly recoveries: number;
|
|
24
|
+
readonly tokens: {
|
|
25
|
+
readonly sum: number;
|
|
26
|
+
readonly covered: number;
|
|
27
|
+
};
|
|
28
|
+
readonly models: readonly {
|
|
29
|
+
readonly model: string;
|
|
30
|
+
readonly tokens: number;
|
|
31
|
+
}[];
|
|
32
|
+
readonly modelCovered: number;
|
|
33
|
+
readonly workerTime: {
|
|
34
|
+
readonly sum: number;
|
|
35
|
+
readonly covered: number;
|
|
36
|
+
};
|
|
37
|
+
readonly goalWall: {
|
|
38
|
+
readonly sum: number;
|
|
39
|
+
readonly covered: number;
|
|
40
|
+
};
|
|
41
|
+
readonly overlap: {
|
|
42
|
+
readonly worker: number;
|
|
43
|
+
readonly wall: number;
|
|
44
|
+
readonly covered: number;
|
|
45
|
+
readonly ratio: number | null;
|
|
46
|
+
};
|
|
47
|
+
readonly titles: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/** Rebuild from retained source records; no cache survives deletion, retention, or reset. */
|
|
50
|
+
export declare function summarizeCareer(histories: readonly (readonly GoalFlightEventRecord[])[], coverage: CareerCoverage): SortieCareer;
|
|
51
|
+
/** One bounded, read-only scan at terminal time. Current records are reused, not fetched again. */
|
|
52
|
+
export declare function collectCareer(directories: readonly string[], currentPath: string, current: readonly GoalFlightEventRecord[], read: (path: string) => Promise<{
|
|
53
|
+
readonly records: readonly GoalFlightEventRecord[];
|
|
54
|
+
}>, maxFiles?: number): Promise<SortieCareer>;
|
|
55
|
+
export declare function renderCareer(career: SortieCareer | undefined): string[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { goalFingerprint } from "../core/goal-bound.js";
|
|
4
|
+
import { validGoalReport } from "../core/goal-report.js";
|
|
5
|
+
/** Rebuild from retained source records; no cache survives deletion, retention, or reset. */
|
|
6
|
+
export function summarizeCareer(histories, coverage) {
|
|
7
|
+
const unique = new Map();
|
|
8
|
+
for (const history of histories)
|
|
9
|
+
for (const record of history)
|
|
10
|
+
unique.set(record.event_hash, record);
|
|
11
|
+
const events = [...unique.values()].sort((a, b) => Date.parse(a.event.at) - Date.parse(b.event.at) || a.sequence - b.sequence);
|
|
12
|
+
const goals = new Map();
|
|
13
|
+
for (const { event } of events) {
|
|
14
|
+
if (!goals.has(event.goal_id) && event.kind !== "goal.accepted")
|
|
15
|
+
continue;
|
|
16
|
+
const goal = goals.get(event.goal_id) ?? { since: null, receipt: null, reports: new Map(), units: new Map() };
|
|
17
|
+
goals.set(event.goal_id, goal);
|
|
18
|
+
if (event.kind === "goal.accepted")
|
|
19
|
+
goal.since ??= event.at;
|
|
20
|
+
if (event.kind === "goal.terminal")
|
|
21
|
+
goal.receipt = event.receipt;
|
|
22
|
+
if (event.kind === "goal.user-continued" || event.kind === "goal.revised")
|
|
23
|
+
goal.receipt = null;
|
|
24
|
+
if (event.kind === "unit.settled")
|
|
25
|
+
goal.units.set(event.reservation_id, event.elapsed_ms);
|
|
26
|
+
if (event.kind === "goal.reported" && validGoalReport(event.report))
|
|
27
|
+
goal.reports.set(event.report.terminal_key, event.report);
|
|
28
|
+
}
|
|
29
|
+
let completed = 0, interrupted = 0, external = 0, decision = 0, active = 0, telemetryCovered = 0, recoveries = 0;
|
|
30
|
+
const firstPass = { count: 0, eligible: 0 }, tokens = { sum: 0, covered: 0 }, workerTime = { sum: 0, covered: 0 }, goalWall = { sum: 0, covered: 0 };
|
|
31
|
+
const models = new Map();
|
|
32
|
+
const overlap = { worker: 0, wall: 0, covered: 0, ratio: null };
|
|
33
|
+
let modelCovered = 0, packTactics = false;
|
|
34
|
+
for (const goal of goals.values()) {
|
|
35
|
+
const receipt = goal.receipt;
|
|
36
|
+
if (receipt === null) {
|
|
37
|
+
active++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (receipt.status === "succeeded")
|
|
41
|
+
completed++;
|
|
42
|
+
else if (receipt.stop_reason === "external_dependency" || receipt.stop_reason === "persistence_unavailable")
|
|
43
|
+
external++;
|
|
44
|
+
else if (receipt.stop_reason === "awaiting_user")
|
|
45
|
+
decision++;
|
|
46
|
+
else
|
|
47
|
+
interrupted++;
|
|
48
|
+
const elapsed = Date.parse(receipt.ended_at) - Date.parse(receipt.started_at);
|
|
49
|
+
if (Number.isFinite(elapsed) && elapsed >= 0) {
|
|
50
|
+
goalWall.sum += elapsed;
|
|
51
|
+
goalWall.covered++;
|
|
52
|
+
}
|
|
53
|
+
if ([...goal.units.values()].every((value) => value !== null && Number.isFinite(value) && value >= 0)) {
|
|
54
|
+
workerTime.sum += [...goal.units.values()].reduce((sum, value) => sum + (value ?? 0), 0);
|
|
55
|
+
workerTime.covered++;
|
|
56
|
+
}
|
|
57
|
+
const report = goal.reports.get(goalFingerprint(receipt));
|
|
58
|
+
if (report === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
telemetryCovered++;
|
|
61
|
+
if (report.overlap !== undefined) {
|
|
62
|
+
overlap.worker += report.overlap.worker_ms;
|
|
63
|
+
overlap.wall += report.overlap.wall_ms;
|
|
64
|
+
overlap.covered++;
|
|
65
|
+
}
|
|
66
|
+
if (report.first_pass_eligible && receipt.status === "succeeded") {
|
|
67
|
+
firstPass.eligible++;
|
|
68
|
+
if (report.traits.includes("clean-sweep"))
|
|
69
|
+
firstPass.count++;
|
|
70
|
+
}
|
|
71
|
+
if (report.traits.includes("recovery") && receipt.status === "succeeded")
|
|
72
|
+
recoveries++;
|
|
73
|
+
if (report.traits.includes("pack-tactics"))
|
|
74
|
+
packTactics = true;
|
|
75
|
+
if (report.tokens !== null) {
|
|
76
|
+
tokens.sum += report.tokens;
|
|
77
|
+
tokens.covered++;
|
|
78
|
+
}
|
|
79
|
+
if (report.models !== null) {
|
|
80
|
+
modelCovered++;
|
|
81
|
+
for (const entry of report.models)
|
|
82
|
+
models.set(entry.model, (models.get(entry.model) ?? 0) + entry.tokens);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const dates = [...goals.values()].flatMap((goal) => goal.since === null ? [] : [goal.since]).sort((a, b) => Date.parse(a) - Date.parse(b));
|
|
86
|
+
if (overlap.wall > 0)
|
|
87
|
+
overlap.ratio = overlap.worker / overlap.wall;
|
|
88
|
+
return { scope: "retained-project-goals", since: dates[0] ?? null, coverage, goals: goals.size,
|
|
89
|
+
completed, interrupted, external, decision, active, telemetryCovered, firstPass, recoveries, tokens,
|
|
90
|
+
models: [...models].sort(([a], [b]) => a.localeCompare(b)).map(([model, tokens]) => ({ model, tokens })), modelCovered,
|
|
91
|
+
workerTime, goalWall, overlap, titles: [
|
|
92
|
+
...(completed > 0 ? ["任務完遂の隊"] : []), ...(packTactics ? ["連携の隊"] : []),
|
|
93
|
+
...(recoveries > 0 ? ["復帰の隊"] : []), ...(firstPass.count > 0 ? ["一発完遂の隊"] : []),
|
|
94
|
+
] };
|
|
95
|
+
}
|
|
96
|
+
/** One bounded, read-only scan at terminal time. Current records are reused, not fetched again. */
|
|
97
|
+
export async function collectCareer(directories, currentPath, current, read, maxFiles = 64) {
|
|
98
|
+
const paths = new Set();
|
|
99
|
+
let unavailable = 0;
|
|
100
|
+
for (const directory of new Set(directories)) {
|
|
101
|
+
try {
|
|
102
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
103
|
+
if (entry.isFile() && /^[a-f0-9]{64}\.json$/u.test(entry.name))
|
|
104
|
+
paths.add(resolve(join(directory, entry.name)));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code !== "ENOENT")
|
|
109
|
+
unavailable++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
paths.delete(resolve(currentPath));
|
|
113
|
+
const selected = [...paths].sort().slice(0, Math.max(0, maxFiles - 1));
|
|
114
|
+
const histories = [current];
|
|
115
|
+
for (const path of selected) {
|
|
116
|
+
try {
|
|
117
|
+
histories.push((await read(path)).records);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
unavailable++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return summarizeCareer(histories, { files: paths.size + 1, included: histories.length, unavailable, truncated: selected.length < paths.size });
|
|
124
|
+
}
|
|
125
|
+
export function renderCareer(career) {
|
|
126
|
+
if (career === undefined)
|
|
127
|
+
return ["**📜 PACK RECORD:** 保存履歴を取得できません"];
|
|
128
|
+
const terminal = career.goals - career.active;
|
|
129
|
+
const firstPass = career.firstPass.eligible === 0 ? "計測不可(対象0件)" : `${career.firstPass.count}/${career.firstPass.eligible}件`;
|
|
130
|
+
const minutes = (metric) => metric.covered === 0 ? "計測不可" : `${(metric.sum / 60000).toFixed(1)}分(${metric.covered}/${terminal}任務)`;
|
|
131
|
+
const models = [...career.models].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
132
|
+
const modelText = models.slice(0, 4).map((entry) => `${entry.model.replace(/[\\`*_{}\[\]()<>!|\r\n]/gu, "").slice(0, 120)} ${entry.tokens.toLocaleString("ja-JP")}`).join(" · ");
|
|
133
|
+
return [
|
|
134
|
+
"**📜 PACK RECORD — 記録済み戦績**",
|
|
135
|
+
`**戦績:** 完了 ${career.completed} · 中断 ${career.interrupted} · 外部待機 ${career.external} · 指示待ち ${career.decision} · 進行中 ${career.active}`,
|
|
136
|
+
`**初回完遂:** ${firstPass} · 復帰 ${career.recoveries}件(計測記録 ${career.telemetryCovered}/${terminal}任務)`,
|
|
137
|
+
`**累積使用量:** ${career.tokens.covered === 0 ? "計測不可" : `${career.tokens.sum.toLocaleString("ja-JP")} tokens`}(計測 ${career.tokens.covered}/${terminal}任務)`,
|
|
138
|
+
`**累積モデル:** ${career.modelCovered === 0 ? "計測不可" : modelText || "出力なし"}${models.length > 4 ? " · ほか" : ""}(token計測 ${career.modelCovered}/${terminal}任務)`,
|
|
139
|
+
`**累積時間:** worker ${minutes(career.workerTime)} · goal期間合計 ${minutes(career.goalWall)}(待機含む・同時刻重複あり)`,
|
|
140
|
+
`**累積実行重複率:** ${career.overlap.ratio === null ? "計測不可" : `${career.overlap.ratio.toFixed(2)}×`}(総和の比・${career.overlap.covered}/${terminal}任務・速度倍率ではありません)`,
|
|
141
|
+
`**保存範囲:** ${career.since?.slice(0, 10) ?? "開始日不明"}以降の現存履歴 · ${career.coverage.included}/${career.coverage.files}ファイル${career.coverage.unavailable || career.coverage.truncated ? " · 部分集計" : ""} · 生涯戦績ではありません`,
|
|
142
|
+
...(career.titles.length ? [`**🎖 隊の称号:** ${career.titles.join(" · ")}`] : []),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { GoalAcceptanceContract, GoalTerminalReceipt, GoalFlightEventRecord } from "../core/goal-bound.js";
|
|
2
|
+
type Span = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
};
|
|
6
|
+
type Check = Span & {
|
|
7
|
+
command: string;
|
|
8
|
+
passed: boolean;
|
|
9
|
+
};
|
|
10
|
+
export interface DebriefSession {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly root: boolean;
|
|
13
|
+
readonly spans: Span[];
|
|
14
|
+
readonly checks: Check[];
|
|
15
|
+
readonly mutations: Span[];
|
|
16
|
+
readonly models: Record<string, number | null>;
|
|
17
|
+
readonly reviews: Array<{
|
|
18
|
+
at: number;
|
|
19
|
+
status: "PASS" | "FAIL" | "WAIVED";
|
|
20
|
+
}>;
|
|
21
|
+
readonly tasks: string[];
|
|
22
|
+
complete: boolean;
|
|
23
|
+
timingComplete: boolean;
|
|
24
|
+
failed: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface DebriefObservation {
|
|
27
|
+
readonly complete: boolean;
|
|
28
|
+
readonly sessions: readonly DebriefSession[];
|
|
29
|
+
readonly window: Span | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface Debrief {
|
|
32
|
+
readonly pack: readonly {
|
|
33
|
+
readonly model: string;
|
|
34
|
+
readonly count: number;
|
|
35
|
+
}[] | null;
|
|
36
|
+
readonly mix: readonly {
|
|
37
|
+
readonly model: string;
|
|
38
|
+
readonly tokens: number;
|
|
39
|
+
readonly percent: number;
|
|
40
|
+
}[] | null;
|
|
41
|
+
readonly validation: "PASS" | "FAIL" | "未確認";
|
|
42
|
+
readonly review: "PASS" | "FAIL" | "WAIVED" | "未確認";
|
|
43
|
+
readonly traits: readonly ("連携作戦" | "修正から復帰" | "一発完遂")[];
|
|
44
|
+
readonly firstPassEligible?: boolean;
|
|
45
|
+
readonly overlap?: {
|
|
46
|
+
readonly workerMilliseconds: number;
|
|
47
|
+
readonly wallMilliseconds: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Extract only bounded typed metadata during the existing host history traversal. No conversation text survives. */
|
|
51
|
+
export declare function observeDebriefSession(id: string, root: boolean, messages: readonly Record<string, unknown>[], window?: Span): DebriefSession;
|
|
52
|
+
export declare function buildDebrief(receipt: GoalTerminalReceipt, contract: GoalAcceptanceContract | null, observation: DebriefObservation | undefined, records?: readonly GoalFlightEventRecord[]): Debrief;
|
|
53
|
+
export declare function renderDebrief(debrief: Debrief | undefined): string[];
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { DEDICATED_WORKER_ROLES, LUNA_FABRIC_WORKER_ROLE } from "./model-routing.js";
|
|
3
|
+
const object = (value) => value !== null && typeof value === "object" ? value : undefined;
|
|
4
|
+
const timeValue = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
5
|
+
const fingerprint = (value) => createHash("sha256").update(value).digest("hex");
|
|
6
|
+
const workerRoles = new Set([...DEDICATED_WORKER_ROLES, LUNA_FABRIC_WORKER_ROLE]);
|
|
7
|
+
function unionDuration(spans) {
|
|
8
|
+
let end = -1, total = 0;
|
|
9
|
+
for (const span of [...spans].sort((a, b) => a.start - b.start)) {
|
|
10
|
+
total += Math.max(0, span.end - Math.max(span.start, end));
|
|
11
|
+
end = Math.max(end, span.end);
|
|
12
|
+
}
|
|
13
|
+
return total;
|
|
14
|
+
}
|
|
15
|
+
const within = (span, window) => span.start <= span.end && (window === undefined || span.start >= window.start && span.end <= window.end);
|
|
16
|
+
const spanOf = (value) => {
|
|
17
|
+
const time = object(value);
|
|
18
|
+
const start = timeValue(time?.start ?? time?.created), end = timeValue(time?.end ?? time?.completed);
|
|
19
|
+
return start === undefined || end === undefined || end < start ? undefined : { start, end };
|
|
20
|
+
};
|
|
21
|
+
/** Extract only bounded typed metadata during the existing host history traversal. No conversation text survives. */
|
|
22
|
+
export function observeDebriefSession(id, root, messages, window) {
|
|
23
|
+
const session = { id, root, spans: [], checks: [], mutations: [], models: {}, reviews: [], tasks: [], complete: true, timingComplete: true, failed: false };
|
|
24
|
+
const calls = new Set();
|
|
25
|
+
for (const message of messages) {
|
|
26
|
+
const info = object(message.info) ?? message;
|
|
27
|
+
const time = info.time ?? message.time;
|
|
28
|
+
// The root's in-flight terminal answer is intentionally outside pre-terminal accounting.
|
|
29
|
+
if (root && object(time) !== undefined && timeValue(object(time)?.completed) === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const span = spanOf(time);
|
|
32
|
+
if (span === undefined) {
|
|
33
|
+
session.complete = false;
|
|
34
|
+
session.timingComplete = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!within(span, window)) {
|
|
38
|
+
if (window !== undefined && span.start < window.end && span.end > window.start) {
|
|
39
|
+
session.complete = false;
|
|
40
|
+
session.timingComplete = false;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
session.spans.push(span);
|
|
45
|
+
if (info.error !== undefined)
|
|
46
|
+
session.failed = true;
|
|
47
|
+
for (const raw of Array.isArray(message.parts) ? message.parts : []) {
|
|
48
|
+
const part = object(raw);
|
|
49
|
+
if (part?.type !== "tool")
|
|
50
|
+
continue;
|
|
51
|
+
const call = typeof part.callID === "string" ? part.callID : typeof part.id === "string" ? part.id : undefined;
|
|
52
|
+
if (call === undefined) {
|
|
53
|
+
session.complete = false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (calls.has(call))
|
|
57
|
+
continue;
|
|
58
|
+
calls.add(call);
|
|
59
|
+
const state = object(part.state);
|
|
60
|
+
const interval = spanOf(state?.time);
|
|
61
|
+
if (state?.status !== "completed" || interval === undefined) {
|
|
62
|
+
session.failed = true;
|
|
63
|
+
session.complete = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!within(interval, window)) {
|
|
67
|
+
session.complete = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const tool = typeof part.tool === "string" ? part.tool.toLowerCase() : "";
|
|
71
|
+
const args = object(state.input);
|
|
72
|
+
if (tool.startsWith("sortie_") && typeof state.output === "string" && state.output.length <= 32768) {
|
|
73
|
+
try {
|
|
74
|
+
if (object(JSON.parse(state.output))?.status === "denied")
|
|
75
|
+
session.failed = true;
|
|
76
|
+
}
|
|
77
|
+
catch { /* Non-JSON output cannot prove a clean controller operation. */
|
|
78
|
+
session.complete = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (tool === "task") {
|
|
82
|
+
const metadata = object(state.metadata);
|
|
83
|
+
const child = metadata?.sessionId ?? metadata?.sessionID;
|
|
84
|
+
if (typeof args?.subagent_type !== "string")
|
|
85
|
+
session.complete = false;
|
|
86
|
+
else if (workerRoles.has(args.subagent_type)) {
|
|
87
|
+
if (typeof child === "string")
|
|
88
|
+
session.tasks.push(child);
|
|
89
|
+
else
|
|
90
|
+
session.complete = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (["edit", "write", "apply_patch"].includes(tool)) {
|
|
94
|
+
const metadata = object(state.metadata);
|
|
95
|
+
const changed = typeof metadata?.diff === "string" && /^[+-](?![+-])/mu.test(metadata.diff) ||
|
|
96
|
+
Array.isArray(metadata?.files) && metadata.files.some((file) => {
|
|
97
|
+
const entry = object(file);
|
|
98
|
+
return typeof entry?.diff === "string" && /^[+-](?![+-])/mu.test(entry.diff);
|
|
99
|
+
});
|
|
100
|
+
if (changed)
|
|
101
|
+
session.mutations.push(interval);
|
|
102
|
+
else
|
|
103
|
+
session.complete = false;
|
|
104
|
+
}
|
|
105
|
+
if (tool === "bash" && typeof args?.command === "string") {
|
|
106
|
+
const metadata = object(state.metadata);
|
|
107
|
+
const exit = metadata?.exit ?? metadata?.exitCode;
|
|
108
|
+
if (typeof exit === "number" && Number.isInteger(exit)) {
|
|
109
|
+
session.checks.push({ ...interval, command: fingerprint(args.command), passed: exit === 0 });
|
|
110
|
+
if (exit !== 0)
|
|
111
|
+
session.failed = true;
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
session.complete = false;
|
|
115
|
+
}
|
|
116
|
+
// Only accepted controller outputs prove a review. Reviewer prose and tool input cannot do so.
|
|
117
|
+
if (tool === "sortie_accept_luna_fabric_candidate" && typeof state.output === "string" && state.output.length <= 32768) {
|
|
118
|
+
try {
|
|
119
|
+
const output = object(JSON.parse(state.output));
|
|
120
|
+
const fabric = object(output?.fabric);
|
|
121
|
+
const review = object(fabric?.review);
|
|
122
|
+
if (output?.status === "accepted" && review?.status === "pass")
|
|
123
|
+
session.reviews.push({ at: interval.end, status: "PASS" });
|
|
124
|
+
else if (output?.status === "accepted" && review?.status === "skip")
|
|
125
|
+
session.reviews.push({ at: interval.end, status: "WAIVED" });
|
|
126
|
+
else if (output?.status === "rejected" && review?.status === "fail")
|
|
127
|
+
session.reviews.push({ at: interval.end, status: "FAIL" });
|
|
128
|
+
}
|
|
129
|
+
catch { /* Unknown evidence stays unknown. */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (session.spans.length > 4096 || calls.size > 4096) {
|
|
133
|
+
session.complete = false;
|
|
134
|
+
session.timingComplete = false;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return session;
|
|
139
|
+
}
|
|
140
|
+
export function buildDebrief(receipt, contract, observation, records) {
|
|
141
|
+
const empty = { pack: null, mix: null, validation: "未確認", review: "未確認", traits: [] };
|
|
142
|
+
if (observation === undefined)
|
|
143
|
+
return empty;
|
|
144
|
+
const sessions = observation.sessions;
|
|
145
|
+
const complete = observation.complete && sessions.every((session) => session.complete);
|
|
146
|
+
const timingComplete = observation.complete && sessions.every((session) => session.timingComplete);
|
|
147
|
+
const children = sessions.filter((session) => !session.root && session.spans.length > 0);
|
|
148
|
+
const counts = new Map(), totals = new Map();
|
|
149
|
+
let usageComplete = timingComplete;
|
|
150
|
+
for (const session of sessions) {
|
|
151
|
+
for (const [model, tokens] of Object.entries(session.models)) {
|
|
152
|
+
if (tokens === null || model === "未分類")
|
|
153
|
+
usageComplete = false;
|
|
154
|
+
else
|
|
155
|
+
totals.set(model, (totals.get(model) ?? 0) + tokens);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const session of children) {
|
|
159
|
+
const models = Object.keys(session.models);
|
|
160
|
+
const label = models.length === 0 ? "未分類" : models.length === 1 ? models[0] : "混成";
|
|
161
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
const total = [...totals.values()].reduce((sum, tokens) => sum + tokens, 0);
|
|
164
|
+
const commands = new Set(contract?.criteria.flatMap((criterion) => criterion.validation_command === undefined ? [] : [fingerprint(criterion.validation_command)]));
|
|
165
|
+
const checks = sessions.flatMap((session) => session.checks.filter((check) => commands.has(check.command))).sort((a, b) => a.end - b.end);
|
|
166
|
+
const latest = new Map();
|
|
167
|
+
for (const check of checks)
|
|
168
|
+
latest.set(check.command, check);
|
|
169
|
+
const validation = [...latest.values()].some((check) => !check.passed) ? "FAIL"
|
|
170
|
+
: complete && commands.size > 0 && latest.size === commands.size ? "PASS" : "未確認";
|
|
171
|
+
const reviews = sessions.flatMap((session) => session.reviews).sort((a, b) => a.at - b.at);
|
|
172
|
+
const traits = [];
|
|
173
|
+
const spans = children.flatMap((session) => session.spans.filter((span) => span.end > span.start)
|
|
174
|
+
.map((span) => ({ ...span, session: session.id }))).sort((a, b) => a.start - b.start);
|
|
175
|
+
let furthest;
|
|
176
|
+
for (const span of spans) {
|
|
177
|
+
if (furthest !== undefined && span.session !== furthest.session && span.start < furthest.end) {
|
|
178
|
+
traits.push("連携作戦");
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (furthest === undefined || span.end > furthest.end)
|
|
182
|
+
furthest = span;
|
|
183
|
+
}
|
|
184
|
+
// Same child and exact declared validator: never combine a sibling's failure with another candidate's success.
|
|
185
|
+
const recovered = children.some((session) => {
|
|
186
|
+
const failures = new Map();
|
|
187
|
+
const mutations = [...session.mutations].sort((a, b) => a.end - b.end);
|
|
188
|
+
let index = 0, lastEditStart = -1;
|
|
189
|
+
for (const check of [...session.checks].sort((a, b) => a.start - b.start)) {
|
|
190
|
+
while (index < mutations.length && mutations[index].end <= check.start) {
|
|
191
|
+
lastEditStart = Math.max(lastEditStart, mutations[index++].start);
|
|
192
|
+
}
|
|
193
|
+
if (!commands.has(check.command))
|
|
194
|
+
continue;
|
|
195
|
+
if (!check.passed)
|
|
196
|
+
failures.set(check.command, check.end);
|
|
197
|
+
else if (failures.has(check.command) && lastEditStart >= failures.get(check.command))
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
});
|
|
202
|
+
if (complete && receipt.status === "succeeded" && validation === "PASS" && recovered)
|
|
203
|
+
traits.push("修正から復帰");
|
|
204
|
+
// Only the complete, terminal-matched goal ledger can prove absence of retries; old/missing ledgers cannot.
|
|
205
|
+
const events = records?.filter(({ event }) => event.goal_id === receipt.goal_id && event.kind !== "goal.reported").map(({ event }) => event);
|
|
206
|
+
const reserved = events?.filter((event) => event.kind === "dispatch.reserved") ?? [];
|
|
207
|
+
const settled = events?.filter((event) => event.kind === "unit.settled") ?? [];
|
|
208
|
+
const tasks = sessions.flatMap((session) => session.tasks);
|
|
209
|
+
const terminal = events?.at(-1);
|
|
210
|
+
const fullLedger = events?.[0]?.kind === "goal.accepted" && terminal?.kind === "goal.terminal" &&
|
|
211
|
+
terminal.receipt.terminal_revision === receipt.terminal_revision && terminal.receipt.status === receipt.status;
|
|
212
|
+
const firstPassEligible = complete && fullLedger && receipt.status === "succeeded" && commands.size > 0 &&
|
|
213
|
+
validation === "PASS" && reserved.length > 0 && reserved.length === settled.length && tasks.length === reserved.length;
|
|
214
|
+
if (firstPassEligible &&
|
|
215
|
+
reserved.length === settled.length && tasks.length === reserved.length && new Set(tasks).size === tasks.length &&
|
|
216
|
+
settled.every((event) => event.disposition === "succeeded" && event.result_class === "acceptance") &&
|
|
217
|
+
!events?.some((event) => event.kind === "goal.replanned" || event.kind === "goal.user-continued" || event.kind === "goal.revised" ||
|
|
218
|
+
(event.kind === "validation.admission" && event.decision === "DENY") || (event.kind === "validation.settled" && event.outcome !== "passed")) &&
|
|
219
|
+
sessions.every((session) => !session.failed && new Set(session.checks.map((check) => check.command)).size === session.checks.length))
|
|
220
|
+
traits.push("一発完遂");
|
|
221
|
+
return { pack: timingComplete ? [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([model, count]) => ({ model, count })) : null,
|
|
222
|
+
mix: usageComplete && total > 0 ? [...totals].sort(([a], [b]) => a.localeCompare(b)).map(([model, tokens]) => ({ model, tokens, percent: tokens / total * 100 })) : null,
|
|
223
|
+
validation, review: complete ? reviews.filter((review) => !sessions.some((session) => session.mutations.some((edit) => edit.end > review.at))).at(-1)?.status ?? "未確認" : "未確認", traits, firstPassEligible,
|
|
224
|
+
...(timingComplete ? { overlap: { workerMilliseconds: children.reduce((sum, child) => sum + unionDuration(child.spans), 0),
|
|
225
|
+
wallMilliseconds: unionDuration(spans) } } : {}) };
|
|
226
|
+
}
|
|
227
|
+
const label = (text) => text.replace(/[\r\n\t]/gu, " ").replace(/[\\`*_{}\[\]()<>!|]/gu, "").slice(0, 120);
|
|
228
|
+
export function renderDebrief(debrief) {
|
|
229
|
+
const pack = debrief?.pack == null ? null : [...debrief.pack].sort((a, b) => b.count - a.count || a.model.localeCompare(b.model));
|
|
230
|
+
const packVisible = pack?.slice(0, 4) ?? [];
|
|
231
|
+
if (pack !== null && pack.length > 4)
|
|
232
|
+
packVisible.push({ model: "その他", count: pack.slice(4).reduce((sum, entry) => sum + entry.count, 0) });
|
|
233
|
+
const mix = debrief?.mix == null ? null : [...debrief.mix].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
234
|
+
const visible = mix?.slice(0, 4) ?? [];
|
|
235
|
+
if (mix !== null && mix.length > 4)
|
|
236
|
+
visible.push({ model: "その他", tokens: mix.slice(4).reduce((sum, entry) => sum + entry.tokens, 0),
|
|
237
|
+
percent: mix.slice(4).reduce((sum, entry) => sum + entry.percent, 0) });
|
|
238
|
+
const bars = (percent) => {
|
|
239
|
+
const filled = Math.max(0, Math.min(10, Math.round(percent / 10)));
|
|
240
|
+
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
241
|
+
};
|
|
242
|
+
return [
|
|
243
|
+
`**🐕 出撃隊:** ${pack === null ? "計測不可" : pack.length === 0 ? "出撃なし" : packVisible.map((entry) => `${label(entry.model)} ×${entry.count}`).join(" · ")}`,
|
|
244
|
+
`**モデル別token内訳:** ${mix === null ? "計測不可" : ""}`,
|
|
245
|
+
...visible.map((entry) => `**↳** ${label(entry.model)} \`${bars(entry.percent)}\` ${entry.percent.toFixed(1)}%`),
|
|
246
|
+
`**実行重複率:** ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
247
|
+
? `${(debrief.overlap.workerMilliseconds / debrief.overlap.wallMilliseconds).toFixed(2)}×(worker区間・速度倍率ではありません)` : "計測不可"}`,
|
|
248
|
+
`**確認:** 対象検証 ${debrief?.validation ?? "未確認"} · 直近Review ${debrief?.review === "WAIVED" ? "免除" : debrief?.review ?? "未確認"}`,
|
|
249
|
+
...(debrief?.traits.length ? [`**🏅 今回の戦績:** ${debrief.traits.join(" · ")}`] : []),
|
|
250
|
+
];
|
|
251
|
+
}
|
package/dist/runtime-assets.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface RuntimeAsset {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const runtimeAssets: readonly [{
|
|
9
9
|
readonly name: "dog-coordinator";
|
|
10
|
-
readonly version: "0.3.
|
|
10
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
11
11
|
readonly installPath: "agent/dog-coordinator.md";
|
|
12
12
|
readonly content: `---
|
|
13
13
|
description: Canonical MkII coordinator packaged by Sortie-dogs
|
|
@@ -1402,8 +1402,14 @@ END_COMMIT_SCOPE_FIXTURE
|
|
|
1402
1402
|
At each checkpoint and terminal return, preserve concise proof internally. The user-facing terminal
|
|
1403
1403
|
return MUST begin with its conclusion: no plan, progress, assessment, Evidence heading, or preamble.
|
|
1404
1404
|
Use exactly one of DONE, INTERRUPTED, BLOCKED, or NEED_DECISION with one status emoji and a short
|
|
1405
|
-
Japanese conclusion. Then render Japanese \u5909\u66F4\u70B9, \u78BA\u8A8D\u7D50\u679C, and \u6B21 paragraphs without bullets or
|
|
1406
|
-
emoji. The plugin injects measured Speed, Cost, and \u9054\u6210 paragraphs
|
|
1405
|
+
Japanese conclusion. Then render Japanese \u5909\u66F4\u70B9, \u78BA\u8A8D\u7D50\u679C, and \u6B21 paragraphs without bullets or decorative
|
|
1406
|
+
emoji. The plugin injects measured Speed, Cost, and \u9054\u6210 paragraphs in a Japanese mission debrief card,
|
|
1407
|
+
with one fixed icon per section, observed pack/model usage, validation/review, and evidence-backed traits.
|
|
1408
|
+
Its Markdown token bars and PACK RECORD summarize retained project goals, with coverage and team titles;
|
|
1409
|
+
they never imply lifetime history, XP, levels, unmeasured savings, or a leaderboard rank.
|
|
1410
|
+
Never write the card, its metrics, or its badges yourself. Do not estimate or fabricate them.
|
|
1411
|
+
Use \u4EFB\u52D9\u5B8C\u4E86 for DONE, \u4E2D\u65AD\u5E30\u9084\uFF08\u672A\u5B8C\u4E86\uFF09 for INTERRUPTED, \u5916\u90E8\u8981\u56E0\u3067\u5F85\u6A5F\uFF08\u672A\u5B8C\u4E86\uFF09 for BLOCKED,
|
|
1412
|
+
and \u6307\u793A\u5F85\u3061\uFF08\u672A\u5B8C\u4E86\uFF09 for NEED_DECISION; preserve the machine status token and first-line checkpoint.
|
|
1407
1413
|
Never render a user-facing Evidence heading, <details> block, evidence reference, internal reason code,
|
|
1408
1414
|
ledger key, or raw status. Keep ordered command/exit/fingerprint history, manifests, evidence refs,
|
|
1409
1415
|
review proof, and terminal receipt append-only in their internal typed ledger and host logs. A concise
|
|
@@ -1423,10 +1429,10 @@ TERMINAL_STATUS_SEMANTICS_FIXTURE
|
|
|
1423
1429
|
END_TERMINAL_STATUS_SEMANTICS_FIXTURE
|
|
1424
1430
|
|
|
1425
1431
|
RUNTIME_ASSET_VERSION_SYNC_FIXTURE
|
|
1426
|
-
runtime_version: 0.3.
|
|
1432
|
+
runtime_version: 0.3.81-mission-debrief-v1
|
|
1427
1433
|
shared_marker: src/asset-version.ts
|
|
1428
|
-
packaged_expectation: test/plugin-loader.test.ts uses 0.3.
|
|
1429
|
-
initialize_expectation: test/initialize.test.ts uses 0.3.
|
|
1434
|
+
packaged_expectation: test/plugin-loader.test.ts uses 0.3.81-mission-debrief-v1
|
|
1435
|
+
initialize_expectation: test/initialize.test.ts uses 0.3.81-mission-debrief-v1
|
|
1430
1436
|
rule: runtime asset versions, shared marker, packaged expectation, and initialize expectation change together
|
|
1431
1437
|
END_RUNTIME_ASSET_VERSION_SYNC_FIXTURE
|
|
1432
1438
|
|
|
@@ -1449,32 +1455,32 @@ END_INTERNAL_TERMINAL_PROOF_FIXTURE
|
|
|
1449
1455
|
`;
|
|
1450
1456
|
}, {
|
|
1451
1457
|
readonly name: "dog-worker";
|
|
1452
|
-
readonly version: "0.3.
|
|
1458
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1453
1459
|
readonly installPath: "agent/dog-worker.md";
|
|
1454
1460
|
readonly content: string;
|
|
1455
1461
|
}, {
|
|
1456
1462
|
readonly name: "dog-luna-worker";
|
|
1457
|
-
readonly version: "0.3.
|
|
1463
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1458
1464
|
readonly installPath: "agent/dog-luna-worker.md";
|
|
1459
1465
|
readonly content: string;
|
|
1460
1466
|
}, {
|
|
1461
1467
|
readonly name: "dog-scout";
|
|
1462
|
-
readonly version: "0.3.
|
|
1468
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1463
1469
|
readonly installPath: "agent/dog-scout.md";
|
|
1464
1470
|
readonly content: "---\ndescription: Bounded evidence scout for dog-coordinator\nmode: subagent\nsteps: 8\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n---\n# dog-scout\n\nAccept one concrete missing_evidence_code: manifest, validation, or owner-risk. Accept only an\nexplicit absolute project_root and a known_paths list of at most four paths from dog-coordinator.\nResolve only that evidence key from those paths under project_root; never resolve a path against the\nsession directory. Use Read only, with at most 120 lines and no more than one read per supplied path.\nDo not resolve a second key, explore, invoke another tool, retry, edit, stage, commit, or become user-facing.\n\nWhen project_root is missing, or a supplied path does not resolve under it, or a resolved path is\nunreadable, report that dispatch defect as the facts for the requested key and name the exact paths.\nDo not retry, guess another root, or answer from an unread path.\n\nReturn exactly one concise JSON object of at most 800 characters with exactly these keys:\nmissing_evidence_code, facts, evidence_paths, risks. Use no Markdown, code fence, commentary, or raw log. Return it only\nto dog-coordinator. Write the facts and risks prose in the language the dispatch uses for its own\nprose; keep the keys, paths, commands, and identifiers verbatim.\n";
|
|
1465
1471
|
}, {
|
|
1466
1472
|
readonly name: "dog-reviewer";
|
|
1467
|
-
readonly version: "0.3.
|
|
1473
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1468
1474
|
readonly installPath: "agent/dog-reviewer.md";
|
|
1469
1475
|
readonly content: "---\ndescription: Independent source reviewer for dog-coordinator\nmode: subagent\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\n read: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n read: false\n---\n# dog-reviewer\n\nAccept only one bounded SourceReview request from dog-coordinator after canonical\nvalidation for one high-risk candidate. Review only the supplied acceptance criteria, exact\nmanifest, changedLogicSummary, supplied changed-code excerpts, and validation evidence. Confirm every acceptance item explicitly\nmaps to at least one changedLogicSummary entry and assess that changed logic against the mapped\nacceptance item. Missing or incomplete coverage is a concrete finding, never PASS.\nRequire one indexed acceptance[i] -> changedLogicSummary[j] mapping line per acceptance item and\nreject a missing index or unequal mapping count before assessing the changed logic.\nDo not request raw logs or full source files, review low-risk candidates, expand scope, or dispatch\nanother agent.\nTreat those supplied fields as the complete bounded SourceReview artifact; use only that artifact and invoke no tools.\nDo not infer that a branch or exemption is absent from source because a prose summary omits it.\nIf the supplied excerpts do not establish a claim, report an evidence gap and request the exact\nbranch/helper excerpt in the next artifact; do not prescribe a source fix for an unproven defect.\n\nReturn one concise PASS or concrete-finding response only to dog-coordinator before the\ncoordinator commit. Write every finding, evidence, and required-fix sentence in the language the\nsupplied artifact uses for its own prose, one statement per line, and keep verdict values,\nidentifiers, paths, and commands verbatim. Do not implement, remediate, resolve blockers, edit,\nstage, commit, or become user-facing. Remain host-routed: do not require or identify a provider, vendor, model, variant,\nor transport.\n";
|
|
1470
1476
|
}, {
|
|
1471
1477
|
readonly name: "dog-advisor";
|
|
1472
|
-
readonly version: "0.3.
|
|
1478
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1473
1479
|
readonly installPath: "agent/dog-advisor.md";
|
|
1474
1480
|
readonly content: "---\ndescription: Focused technical advisor for dog-coordinator\nmode: subagent\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\n read: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n read: false\n---\n# dog-advisor\n\nAccept only one bounded Strategy request from dog-coordinator for one candidate and one focused\nquestion. Use only the supplied acceptance criteria, exact manifest, constraints, and concise\nevidence. Do not request raw logs or full source files, expand scope, or dispatch another agent.\nTreat those supplied fields as the complete bounded Strategy artifact; use only that artifact and invoke no tools.\nReject every SourceReview request and return the rejection only to dog-coordinator; SourceReview is\ndog-reviewer-only work.\n\nReturn concise options and one recommendation only to dog-coordinator. Write every option,\nrecommendation, and consideration in the language the supplied request uses for its own prose, one\nstatement per line, and keep identifiers, paths, and commands verbatim. Do not perform\nSourceReview, implement, remediate, resolve blockers, edit, stage, commit, or become user-facing.\nImplementation remains dog-worker work. Remain host-routed: do not require or identify a\nprovider, vendor, model, variant, or transport.\n";
|
|
1475
1481
|
}, {
|
|
1476
1482
|
readonly name: "sortie";
|
|
1477
|
-
readonly version: "0.3.
|
|
1483
|
+
readonly version: "0.3.81-mission-debrief-v1";
|
|
1478
1484
|
readonly installPath: "command/sortie.md";
|
|
1479
1485
|
readonly content: "---\ndescription: Start the canonical Sortie-dogs MkII workflow\nagent: dog-coordinator\n---\nRequest: $ARGUMENTS\n\n1. If $ARGUMENTS is empty, request task context and stop; give project init guidance first.\n2. Do not preflight installed runtime assets. The plugin reports version skew without adding model\n turns; proceed from task evidence and project instructions.\n3. On restart or re-entry, reconstruct context from project-local durable artifacts and the\n latest bounded handoff or checkpoint. Preserve both manifests and ordered validation history;\n resume the same task through dog-coordinator with only the required delta.\n4. Otherwise transfer request and project context to dog-coordinator. Frontmatter is the single coordinator\n transfer; never route a worker to the user.\n";
|
|
1480
1486
|
}];
|
package/dist/runtime-assets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GOAL_DECLARATION_FORMAT } from "./core/goal-declaration-format.js";
|
|
2
|
-
const ASSET_VERSION = "0.3.
|
|
2
|
+
const ASSET_VERSION = "0.3.81-mission-debrief-v1";
|
|
3
3
|
// Kept local so source-mode CLI execution does not load the plugin graph.
|
|
4
4
|
const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
5
5
|
const PARALLEL_PREPARE_CAPABILITY = "sortie_prepare_parallel_dispatch";
|
|
@@ -1609,8 +1609,14 @@ END_COMMIT_SCOPE_FIXTURE
|
|
|
1609
1609
|
At each checkpoint and terminal return, preserve concise proof internally. The user-facing terminal
|
|
1610
1610
|
return MUST begin with its conclusion: no plan, progress, assessment, Evidence heading, or preamble.
|
|
1611
1611
|
Use exactly one of DONE, INTERRUPTED, BLOCKED, or NEED_DECISION with one status emoji and a short
|
|
1612
|
-
Japanese conclusion. Then render Japanese 変更点, 確認結果, and 次 paragraphs without bullets or
|
|
1613
|
-
emoji. The plugin injects measured Speed, Cost, and 達成 paragraphs
|
|
1612
|
+
Japanese conclusion. Then render Japanese 変更点, 確認結果, and 次 paragraphs without bullets or decorative
|
|
1613
|
+
emoji. The plugin injects measured Speed, Cost, and 達成 paragraphs in a Japanese mission debrief card,
|
|
1614
|
+
with one fixed icon per section, observed pack/model usage, validation/review, and evidence-backed traits.
|
|
1615
|
+
Its Markdown token bars and PACK RECORD summarize retained project goals, with coverage and team titles;
|
|
1616
|
+
they never imply lifetime history, XP, levels, unmeasured savings, or a leaderboard rank.
|
|
1617
|
+
Never write the card, its metrics, or its badges yourself. Do not estimate or fabricate them.
|
|
1618
|
+
Use 任務完了 for DONE, 中断帰還(未完了) for INTERRUPTED, 外部要因で待機(未完了) for BLOCKED,
|
|
1619
|
+
and 指示待ち(未完了) for NEED_DECISION; preserve the machine status token and first-line checkpoint.
|
|
1614
1620
|
Never render a user-facing Evidence heading, <details> block, evidence reference, internal reason code,
|
|
1615
1621
|
ledger key, or raw status. Keep ordered command/exit/fingerprint history, manifests, evidence refs,
|
|
1616
1622
|
review proof, and terminal receipt append-only in their internal typed ledger and host logs. A concise
|