sortie-dogs 0.15.1 → 0.15.2
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 +7 -7
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-report.d.ts +2 -0
- package/dist/core/goal-report.js +3 -1
- package/dist/core/rare-awards.d.ts +26 -0
- package/dist/core/rare-awards.js +53 -0
- package/dist/core/work-quantity.js +1 -1
- package/dist/plugin/index.js +6 -3
- package/dist/plugin/profiled.js +9 -7
- package/dist/plugin/receipt-presentation.js +2 -2
- package/dist/plugin/report-locale.d.ts +7 -0
- package/dist/plugin/report-locale.js +6 -0
- package/dist/plugin/run-metrics.d.ts +5 -2
- package/dist/plugin/run-metrics.js +92 -52
- package/dist/plugin/sortie-career.d.ts +8 -1
- package/dist/plugin/sortie-career.js +59 -18
- package/dist/plugin/sortie-debrief.d.ts +16 -2
- package/dist/plugin/sortie-debrief.js +108 -25
- package/dist/runtime-mission-assets.d.ts +2 -0
- package/dist/runtime-mission-assets.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**
|
|
23
|
-
· [
|
|
22
|
+
**Current release: [v0.15.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.15.2)**
|
|
23
|
+
· [Release notes](docs/release-v0.15.2.md)
|
|
24
24
|
· [Execution vision](docs/v0.14.0-execution-vision.md)
|
|
25
25
|
|
|
26
26
|
[When to use](#when-to-use-sortie-dogs) · [Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
@@ -78,7 +78,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
78
78
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
79
79
|
|
|
80
80
|
```sh
|
|
81
|
-
npm install --save-dev sortie-dogs@0.
|
|
81
|
+
npm install --save-dev sortie-dogs@0.15.2
|
|
82
82
|
npx --no-install sortie-dogs codex init .
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -107,7 +107,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
107
107
|
Use **OpenCode V2**. In the target project:
|
|
108
108
|
|
|
109
109
|
```sh
|
|
110
|
-
npm install --save-dev sortie-dogs@0.
|
|
110
|
+
npm install --save-dev sortie-dogs@0.15.2
|
|
111
111
|
npx --no-install sortie-dogs init .
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -127,7 +127,7 @@ v0.10 is installed. [OpenCode setup and configuration](docs/configuration.md) ·
|
|
|
127
127
|
|
|
128
128
|
### Project Memory (OpenCode v010, opt-in)
|
|
129
129
|
|
|
130
|
-
With
|
|
130
|
+
With v0.15.2 installed, run `sortie-dogs memory on .` once and reload
|
|
131
131
|
the project plugin. Ordinary public turns then become pending sources; the existing
|
|
132
132
|
Operator records semantic additions, corrections, withdrawals and decisions without
|
|
133
133
|
another save request or a dedicated model call per turn. New sessions recall the
|
|
@@ -284,7 +284,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
284
284
|
|
|
285
285
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
286
286
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
287
|
-
The
|
|
287
|
+
The current Mission asset marker is `0.15.2-tactical-return-v1`; the Codex skill marker is
|
|
288
288
|
`0.14.6-codex-skill-v1`. Installed markers alone do not prove a running host reloaded the new version.
|
|
289
289
|
|
|
290
290
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -300,7 +300,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
300
300
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
301
301
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
302
302
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
303
|
-
- **Releases:** [v0.14.9](docs/release-v0.14.9.md) · [v0.14.8](docs/release-v0.14.8.md) · [v0.14.7](docs/release-v0.14.7.md) · [v0.14.6](docs/release-v0.14.6.md) · [v0.14.5](docs/release-v0.14.5.md) · [v0.14.4](docs/release-v0.14.4.md) · [v0.14.3](docs/release-v0.14.3.md) · [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [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) ·
|
|
303
|
+
- **Releases:** [v0.15.2](docs/release-v0.15.2.md) · [v0.15.1](docs/v0.15.1-migration.md) · [v0.15.0](docs/release-v0.15.0.md) · [v0.14.9](docs/release-v0.14.9.md) · [v0.14.8](docs/release-v0.14.8.md) · [v0.14.7](docs/release-v0.14.7.md) · [v0.14.6](docs/release-v0.14.6.md) · [v0.14.5](docs/release-v0.14.5.md) · [v0.14.4](docs/release-v0.14.4.md) · [v0.14.3](docs/release-v0.14.3.md) · [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [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) ·
|
|
304
304
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
305
305
|
|
|
306
306
|
## Support
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.15.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.15.2-tactical-return-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.15.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.15.2-tactical-return-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.14.6-codex-skill-v1";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RareAwards } from "./rare-awards.ts";
|
|
1
2
|
/** Optional, bounded telemetry retained with the existing terminal goal ledger, never an authority input. */
|
|
2
3
|
export interface GoalReport {
|
|
3
4
|
readonly definition: "pre-terminal-host-tokens/v1";
|
|
@@ -14,5 +15,6 @@ export interface GoalReport {
|
|
|
14
15
|
readonly worker_ms: number;
|
|
15
16
|
readonly wall_ms: number;
|
|
16
17
|
};
|
|
18
|
+
readonly rare?: RareAwards;
|
|
17
19
|
}
|
|
18
20
|
export declare function validGoalReport(value: unknown): value is GoalReport;
|
package/dist/core/goal-report.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validRareAwards } from "./rare-awards.js";
|
|
1
2
|
export function validGoalReport(value) {
|
|
2
3
|
if (value === null || typeof value !== "object")
|
|
3
4
|
return false;
|
|
@@ -5,11 +6,12 @@ export function validGoalReport(value) {
|
|
|
5
6
|
const amount = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
6
7
|
const duration = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
7
8
|
const overlap = item.overlap;
|
|
8
|
-
return Object.keys(item).every((key) => ["definition", "terminal_key", "tokens", "models", "first_pass_eligible", "traits", "overlap"].includes(key)) &&
|
|
9
|
+
return Object.keys(item).every((key) => ["definition", "terminal_key", "tokens", "models", "first_pass_eligible", "traits", "overlap", "rare"].includes(key)) &&
|
|
9
10
|
item.definition === "pre-terminal-host-tokens/v1" && /^sha256:[a-f0-9]{64}$/u.test(item.terminal_key) &&
|
|
10
11
|
(item.tokens === null || amount(item.tokens)) && typeof item.first_pass_eligible === "boolean" &&
|
|
11
12
|
Array.isArray(item.traits) && item.traits.length <= 3 && new Set(item.traits).size === item.traits.length &&
|
|
12
13
|
item.traits.every((trait) => ["pack-tactics", "recovery", "clean-sweep"].includes(trait)) &&
|
|
14
|
+
(item.rare === undefined || validRareAwards(item.rare)) &&
|
|
13
15
|
(overlap === undefined || overlap !== null && typeof overlap === "object" &&
|
|
14
16
|
Object.keys(overlap).every((key) => ["definition", "worker_ms", "wall_ms"].includes(key)) && overlap.definition === "worker-span-union/v1" &&
|
|
15
17
|
duration(overlap.worker_ms) && duration(overlap.wall_ms) && overlap.worker_ms >= overlap.wall_ms) &&
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Presentation telemetry only. Never an execution, review, or acceptance input. */
|
|
2
|
+
export type ConditionState = "true" | "false" | "unknown";
|
|
3
|
+
export type RareStatus = "earned" | "not-earned" | "unknown";
|
|
4
|
+
export interface RareEvaluation {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly rule_version: number;
|
|
7
|
+
readonly status: RareStatus;
|
|
8
|
+
readonly evidence: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface RareAwards {
|
|
11
|
+
readonly definition: string;
|
|
12
|
+
readonly evaluations: readonly RareEvaluation[];
|
|
13
|
+
}
|
|
14
|
+
export declare const RARE_RULES: readonly [{
|
|
15
|
+
readonly id: "rare.clean-sweep";
|
|
16
|
+
readonly rule_version: 1;
|
|
17
|
+
}, {
|
|
18
|
+
readonly id: "rare.recovery-coordination";
|
|
19
|
+
readonly rule_version: 1;
|
|
20
|
+
}];
|
|
21
|
+
/** False dominates unknown for an AND; absence of a positive trait is not a negative proof. */
|
|
22
|
+
export declare function evaluateRareAwards(succeeded: boolean, clean: ConditionState, recovery: ConditionState, coordination: ConditionState): RareAwards;
|
|
23
|
+
/** Bounded forward-compatible envelope: unknown definitions/rules remain telemetry, never awards. */
|
|
24
|
+
export declare function validRareAwards(value: unknown): value is RareAwards;
|
|
25
|
+
/** Recognize only this version and its host evidence contract, not a future rule's success string. */
|
|
26
|
+
export declare function rareEvaluation(rare: RareAwards | undefined, id: string): RareEvaluation | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const RARE_RULES = [
|
|
2
|
+
{ id: "rare.clean-sweep", rule_version: 1 },
|
|
3
|
+
{ id: "rare.recovery-coordination", rule_version: 1 },
|
|
4
|
+
];
|
|
5
|
+
/** False dominates unknown for an AND; absence of a positive trait is not a negative proof. */
|
|
6
|
+
export function evaluateRareAwards(succeeded, clean, recovery, coordination) {
|
|
7
|
+
const status = (conditions) => !succeeded || conditions.includes("false")
|
|
8
|
+
? "not-earned" : conditions.includes("unknown") ? "unknown" : "earned";
|
|
9
|
+
return { definition: "rare-awards/v1", evaluations: [
|
|
10
|
+
{ id: RARE_RULES[0].id, rule_version: 1, status: status([clean]),
|
|
11
|
+
evidence: [succeeded ? "receipt.succeeded" : "receipt.not-succeeded", `clean-sweep.${clean}`] },
|
|
12
|
+
{ id: RARE_RULES[1].id, rule_version: 1, status: status([recovery, coordination]),
|
|
13
|
+
evidence: [succeeded ? "receipt.succeeded" : "receipt.not-succeeded", `recovery.${recovery}`, `coordination.${coordination}`] },
|
|
14
|
+
] };
|
|
15
|
+
}
|
|
16
|
+
/** Bounded forward-compatible envelope: unknown definitions/rules remain telemetry, never awards. */
|
|
17
|
+
export function validRareAwards(value) {
|
|
18
|
+
if (value === null || typeof value !== "object")
|
|
19
|
+
return false;
|
|
20
|
+
const rare = value;
|
|
21
|
+
const code = (v) => typeof v === "string" && /^[a-zA-Z0-9][a-zA-Z0-9./_-]{0,95}$/u.test(v);
|
|
22
|
+
return Object.keys(rare).every(key => ["definition", "evaluations"].includes(key)) && code(rare.definition) &&
|
|
23
|
+
Array.isArray(rare.evaluations) && rare.evaluations.length <= 16 &&
|
|
24
|
+
new Set(rare.evaluations.map(e => e && `${e.id}/${e.rule_version}`)).size === rare.evaluations.length &&
|
|
25
|
+
rare.evaluations.every(e => e !== null && typeof e === "object" &&
|
|
26
|
+
Object.keys(e).every(key => ["id", "rule_version", "status", "evidence"].includes(key)) && code(e.id) &&
|
|
27
|
+
Number.isSafeInteger(e.rule_version) && e.rule_version > 0 && ["earned", "not-earned", "unknown"].includes(e.status) &&
|
|
28
|
+
Array.isArray(e.evidence) && e.evidence.length <= 8 && new Set(e.evidence).size === e.evidence.length && e.evidence.every(code));
|
|
29
|
+
}
|
|
30
|
+
/** Recognize only this version and its host evidence contract, not a future rule's success string. */
|
|
31
|
+
export function rareEvaluation(rare, id) {
|
|
32
|
+
if (rare?.definition !== "rare-awards/v1")
|
|
33
|
+
return undefined;
|
|
34
|
+
const rule = RARE_RULES.find(rule => rule.id === id);
|
|
35
|
+
const evaluation = rare.evaluations.find(e => e.id === id && e.rule_version === rule?.rule_version);
|
|
36
|
+
if (evaluation === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const codes = evaluation.evidence;
|
|
39
|
+
const state = (prefix) => {
|
|
40
|
+
const values = ["true", "false", "unknown"].filter(s => codes.includes(`${prefix}.${s}`));
|
|
41
|
+
return values.length === 1 ? values[0] : undefined;
|
|
42
|
+
};
|
|
43
|
+
const succeeded = codes.includes("receipt.succeeded");
|
|
44
|
+
if (succeeded === codes.includes("receipt.not-succeeded"))
|
|
45
|
+
return undefined;
|
|
46
|
+
const clean = state("clean-sweep"), recovery = state("recovery"), coordination = state("coordination");
|
|
47
|
+
if (id === RARE_RULES[0].id ? clean === undefined : recovery === undefined || coordination === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
const expected = evaluateRareAwards(succeeded, clean ?? "unknown", recovery ?? "unknown", coordination ?? "unknown")
|
|
50
|
+
.evaluations.find(e => e.id === id);
|
|
51
|
+
return expected.status === evaluation.status && expected.evidence.length === codes.length &&
|
|
52
|
+
expected.evidence.every(code => codes.includes(code)) ? evaluation : undefined;
|
|
53
|
+
}
|
|
@@ -495,5 +495,5 @@ export function projectWorkQuantity(state, completed = false, cancelled = false)
|
|
|
495
495
|
remaining_kinds: accepted ? [] : state.remainingKinds };
|
|
496
496
|
}
|
|
497
497
|
export function formatWorkQuantity(progress) {
|
|
498
|
-
return `全体作業量 ${progress.n}
|
|
498
|
+
return `全体作業量 ${progress.n}%(概算)${progress.estimate_revision > 1 ? " · 見積更新" : ""}`;
|
|
499
499
|
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -48,7 +48,7 @@ import { TerminalRescueRuntime } from "../core/terminal-rescue-runtime.js";
|
|
|
48
48
|
import { OpenCodeTerminalRescueHost } from "./terminal-rescue-host.js";
|
|
49
49
|
import { AdaptiveRemediationRuntime, AdaptiveRunFlightLineage } from "../core/adaptive-remediation-runtime.js";
|
|
50
50
|
import { DEFAULT_ADAPTIVE_REMEDIATION_MODEL, GitAdaptiveRemediationHost, OpenCodeAdaptiveRemediationProvider } from "./adaptive-remediation-host.js";
|
|
51
|
-
import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
51
|
+
import { collectRunMetrics, createSortieResult, createGoalReport, withSavedGoalReport, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
52
52
|
import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
53
53
|
import { reviewerCorrectionValidationFresh } from "./mission-review.js";
|
|
54
54
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
@@ -6474,8 +6474,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6474
6474
|
let records = terminal.records;
|
|
6475
6475
|
if (!records.some(({ event }) => event.kind === "goal.reported" && event.report?.terminal_key === report.terminal_key)) {
|
|
6476
6476
|
await ledger.appendGoal({ kind: "goal.reported", at: new Date().toISOString(), goal_id: terminal.receipt.goal_id, report });
|
|
6477
|
-
records = (await ledger.readGoal()).records;
|
|
6478
6477
|
}
|
|
6478
|
+
// appendGoal serializes competing deliveries and retains the first report for this receipt.
|
|
6479
|
+
records = (await ledger.readGoal()).records;
|
|
6480
|
+
sortieResult = withSavedGoalReport(sortieResult, terminal.receipt, records);
|
|
6479
6481
|
const currentPath = goalLedgerFiles.get(goalRoot(textInput.sessionID));
|
|
6480
6482
|
if (currentPath !== undefined) {
|
|
6481
6483
|
const { collectCareer } = await import("./sortie-career.js");
|
|
@@ -7956,8 +7958,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7956
7958
|
const report = createGoalReport(result, receipt);
|
|
7957
7959
|
if (!records.some(({ event }) => event.kind === "goal.reported" && event.report.terminal_key === report.terminal_key)) {
|
|
7958
7960
|
await ledger.appendGoal({ kind: "goal.reported", at: new Date().toISOString(), goal_id: receipt.goal_id, report });
|
|
7959
|
-
records = (await ledger.readGoal()).records;
|
|
7960
7961
|
}
|
|
7962
|
+
records = (await ledger.readGoal()).records;
|
|
7963
|
+
result = withSavedGoalReport(result, receipt, records);
|
|
7961
7964
|
const currentPath = goalLedgerFiles.get(goalRoot(root));
|
|
7962
7965
|
if (currentPath) {
|
|
7963
7966
|
const { collectCareer } = await import("./sortie-career.js");
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
10
10
|
import { BUILT_IN_MODEL_CATALOG } from "./model-routing.js";
|
|
11
11
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
12
12
|
import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentation.js";
|
|
13
|
+
import { reportText, resolveReportLocale } from "./report-locale.js";
|
|
13
14
|
import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
14
15
|
import { normalizeCommand, canonicalObservedValidationMembers, extractWritePaths, isShellTool } from "./gate.js";
|
|
15
16
|
import { normalizeExecutionScope, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
|
|
@@ -1912,16 +1913,17 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1912
1913
|
}
|
|
1913
1914
|
let panel;
|
|
1914
1915
|
if (input.returnReportTransport === "tool-result" && result.receipt?.status === "succeeded") {
|
|
1916
|
+
const t = reportText(resolveReportLocale());
|
|
1915
1917
|
const reviewGaps = missionReportReview(mission, state.runID) === "evidence-gaps"
|
|
1916
|
-
? mission.review.result?.trim() || "独立Reviewの未解決証拠あり" : undefined;
|
|
1918
|
+
? mission.review.result?.trim() || t("独立Reviewの未解決証拠あり", "Independent Review has unconfirmed evidence") : undefined;
|
|
1917
1919
|
const gapSummary = reviewGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").slice(0, 500);
|
|
1918
1920
|
const text = withMissionOperationOutcome(`✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.`, mission) + `\n\n` +
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
(mission?.review ? `\n
|
|
1921
|
+
`**${t("変更点", "Changes")}:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
|
|
1922
|
+
`**${t("確認結果", "Validation")}:** ${t("宣言検証合格", "Declared checks passed")} — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}` +
|
|
1923
|
+
(mission?.review ? `\n${t("レビュー", "Review")}: ${mission.review.verdict === "self-rechecked" ? t("修正著者の自己再確認(独立PASSではない)", "Author self-rechecked (not independent PASS)") : mission.review.verdict === "evidence-gaps" ? t("証拠不足を残して受入れ(レビューPASSではない)", "Accepted with evidence gaps (not Review PASS)") : mission.review.verdict}.` : "") +
|
|
1922
1924
|
(mission?.deliveryObservation?.head ? `\nCommit: ${mission.deliveryObservation.head} (${mission.deliveryObservation.branch ?? "detached"}; clean=${mission.deliveryObservation.clean})` : "") +
|
|
1923
|
-
(reviewGaps ? `\n\n
|
|
1924
|
-
:
|
|
1925
|
+
(reviewGaps ? `\n\n**${t("レビュー補足", "Review note")}:** ${gapSummary}\n\n**${t("次", "Next")}:** ${t("なし(補足だけを理由に再レビュー不要)", "None (no repeat review solely for the advisory)")}`
|
|
1926
|
+
: `\n\n**${t("次", "Next")}:** ${t("なし", "None")}`);
|
|
1925
1927
|
const rendered = await control.renderReturnReport(context.sessionID, text, goalFingerprint(result.receipt), missionReportReview(mission, state.runID), missionReportReviewGaps(mission, state.runID)).catch(() => undefined);
|
|
1926
1928
|
if (rendered)
|
|
1927
1929
|
panel = returnReportPanel(rendered);
|
|
@@ -4914,7 +4916,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
4914
4916
|
const mission = run ? await missions.read(request.sessionID) : undefined;
|
|
4915
4917
|
output.text = await control.renderReturnReport(request.sessionID, output.text, goalFingerprint(receipt), run ? missionReportReview(mission, run.runID) : undefined, run ? missionReportReviewGaps(mission, run.runID) : undefined) ?? output.text;
|
|
4916
4918
|
}
|
|
4917
|
-
if (output.text
|
|
4919
|
+
if (returnReportPanel(output.text) !== undefined)
|
|
4918
4920
|
rememberRendered(`${request.sessionID}:${request.messageID}:${request.partID}`, output.text);
|
|
4919
4921
|
}
|
|
4920
4922
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { terminalRunOutcome } from "./run-metrics.js";
|
|
2
2
|
/** Extract only the host card, excluding the terminal prose that follows it. */
|
|
3
3
|
export function returnReportPanel(rendered) {
|
|
4
|
-
const summary =
|
|
5
|
-
if (summary
|
|
4
|
+
const summary = /<summary><strong>🐾 SORTIE DOGS — (?:帰還報告|Return Report)(?=||<\/strong>)/u.exec(rendered)?.index;
|
|
5
|
+
if (summary === undefined)
|
|
6
6
|
return undefined;
|
|
7
7
|
const start = rendered.lastIndexOf("<details", summary);
|
|
8
8
|
const end = rendered.indexOf("\n</details>", summary);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Product display locale, independent of the language used to instruct the model.
|
|
2
|
+
* Resolve on each delivery, not at plugin creation. Omitted settings preserve the
|
|
3
|
+
* existing Japanese UI; unsupported explicit locales fall back to English.
|
|
4
|
+
*/
|
|
5
|
+
export type ReportLocale = "ja" | "en";
|
|
6
|
+
export declare function resolveReportLocale(value?: string | undefined): ReportLocale;
|
|
7
|
+
export declare const reportText: (locale: ReportLocale) => (ja: string, en: string) => string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function resolveReportLocale(value = process.env.SORTIE_REPORT_LOCALE) {
|
|
2
|
+
if (value === undefined || value.trim() === "")
|
|
3
|
+
return "ja";
|
|
4
|
+
return /^ja(?:[-_]|$)/iu.test(value.trim()) ? "ja" : "en";
|
|
5
|
+
}
|
|
6
|
+
export const reportText = (locale) => (ja, en) => locale === "ja" ? ja : en;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GoalFlightState, GoalTerminalReceipt, GoalFlightEventRecord } from "../core/goal-bound.js";
|
|
2
2
|
import { type Debrief, type DebriefObservation } from "./sortie-debrief.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type GoalReport } from "../core/goal-report.ts";
|
|
4
4
|
import { type SortieCareer } from "./sortie-career.ts";
|
|
5
5
|
export interface RunMetricsClient {
|
|
6
6
|
readonly session?: {
|
|
@@ -113,6 +113,7 @@ export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: S
|
|
|
113
113
|
export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
|
|
114
114
|
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
|
|
115
115
|
export interface SortieResultPresentation {
|
|
116
|
+
readonly locale?: string;
|
|
116
117
|
readonly implementation?: string;
|
|
117
118
|
readonly pending?: string;
|
|
118
119
|
readonly next?: string;
|
|
@@ -131,6 +132,8 @@ export declare function replaceTerminalStatus(text: string, replacement: string)
|
|
|
131
132
|
export declare function replaceDoneTerminalStatus(text: string, replacement: string): string;
|
|
132
133
|
export declare function sanitizeTerminalReport(text: string): string;
|
|
133
134
|
export declare function insertRunMetrics(text: string, metrics: RunMetrics): string;
|
|
134
|
-
export declare function insertSortieResult(text: string, result: SortieResult, missionReview?: SortieResultPresentation["missionReview"], reviewEvidenceGaps?: string): string;
|
|
135
|
+
export declare function insertSortieResult(text: string, result: SortieResult, missionReview?: SortieResultPresentation["missionReview"], reviewEvidenceGaps?: string, localeSetting?: string): string;
|
|
135
136
|
export declare function createGoalReport(result: SortieResult, receipt: GoalTerminalReceipt): GoalReport;
|
|
137
|
+
/** Both terminal delivery callers use the first persisted snapshot, not a later remeasurement. */
|
|
138
|
+
export declare function withSavedGoalReport(result: SortieResult, receipt: GoalTerminalReceipt, records: readonly GoalFlightEventRecord[]): SortieResult;
|
|
136
139
|
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { buildDebrief, renderDebrief, renderDebriefProof, observeDebriefSession } from "./sortie-debrief.js";
|
|
1
|
+
import { buildDebrief, renderDebrief, renderDebriefProof, renderDebriefTraits, renderDebriefTraitEvidence, observeDebriefSession } from "./sortie-debrief.js";
|
|
2
2
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
3
|
+
import { validGoalReport } from "../core/goal-report.js";
|
|
3
4
|
import { renderCareer } from "./sortie-career.js";
|
|
4
5
|
import { estimateModelUsageCost } from "./model-cost.js";
|
|
6
|
+
import { reportText, resolveReportLocale } from "./report-locale.js";
|
|
5
7
|
const unavailable = (reason) => ({ availability: "unavailable", value: null, reason });
|
|
6
8
|
const available = (value, provenance) => ({ availability: "available", value, provenance });
|
|
7
9
|
function elapsedBetween(start, end) {
|
|
@@ -405,77 +407,97 @@ function duration(milliseconds) {
|
|
|
405
407
|
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
406
408
|
return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
407
409
|
}
|
|
408
|
-
function metricText(metric, render) {
|
|
409
|
-
return metric.availability === "available" ? render(metric.value) :
|
|
410
|
+
function metricText(metric, render, unavailable = "計測不可") {
|
|
411
|
+
return metric.availability === "available" ? render(metric.value) : unavailable;
|
|
410
412
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
/** Literal display text, not Markdown supplied by the model/user. Entities are
|
|
414
|
+
* decoded by the renderer after Markdown tokenization. wbr supplies layout-only
|
|
415
|
+
* breaks for long IDs without adding characters to their text/copy value.
|
|
416
|
+
*/
|
|
417
|
+
const reportLiteral = (text) => {
|
|
418
|
+
const escape = (value) => value.replace(/[&<>\\`*_~\[\]#!|]/gu, character => `&#${character.codePointAt(0)};`);
|
|
419
|
+
return text.replace(/[^\s]+/gu, token => {
|
|
420
|
+
const chunks = /^[\x21-\x7e]{33,}$/u.test(token) ? token.match(/.{1,24}/gu) : [token];
|
|
421
|
+
return chunks.map(escape).join("<wbr>");
|
|
422
|
+
}).replace(/^([ ]*)([-+])/u, (_match, space, bullet) => `${space}&#${bullet.codePointAt(0)};`)
|
|
423
|
+
.replace(/^(\d+)([.)])(?=\s)/u, (_match, count, delimiter) => `${count}&#${delimiter.codePointAt(0)};`);
|
|
416
424
|
};
|
|
417
425
|
export function formatSortieResult(result, presentation = {}) {
|
|
418
|
-
const
|
|
426
|
+
const locale = resolveReportLocale(presentation.locale);
|
|
427
|
+
const t = reportText(locale);
|
|
428
|
+
const metric = (value, render) => metricText(value, render, t("計測不可", "Unavailable"));
|
|
429
|
+
const display = (value) => value?.replace(/[\r\n\t]+/gu, " ").trim() || t("未取得", "Unavailable");
|
|
430
|
+
const criteria = metric(result.proof.criteria, (entries) => {
|
|
419
431
|
const passing = entries.filter(({ status }) => status === "PASS").length;
|
|
420
432
|
return `${passing}/${entries.length}`;
|
|
421
433
|
});
|
|
422
434
|
const achievement = result.mission.status === "COMPLETED" ? "COMPLETED"
|
|
423
435
|
: result.mission.status === "INTERRUPTED" ? "INTERRUPTED"
|
|
424
436
|
: result.mission.status === "EXTERNAL_BLOCKER" ? "EXTERNAL_BLOCKER" : "USER_DECISION";
|
|
425
|
-
const summaryLabel = result.mission.status === "COMPLETED" ? "完了"
|
|
426
|
-
: result.mission.status === "INTERRUPTED" ? "中断(未完了)"
|
|
427
|
-
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了" : "ユーザー判断待ち(未完了)";
|
|
437
|
+
const summaryLabel = result.mission.status === "COMPLETED" ? t("完了", "Complete")
|
|
438
|
+
: result.mission.status === "INTERRUPTED" ? t("中断(未完了)", "Interrupted (unfinished)")
|
|
439
|
+
: result.mission.status === "EXTERNAL_BLOCKER" ? t("外部要因で未完了", "External blocker (unfinished)") : t("ユーザー判断待ち(未完了)", "Awaiting user decision (unfinished)");
|
|
428
440
|
const color = result.mission.status === "COMPLETED" ? "🟢" : result.mission.status === "EXTERNAL_BLOCKER" ? "🔴" : "🟡";
|
|
429
|
-
const proof = renderDebriefProof(result.debrief);
|
|
430
|
-
const review = presentation.missionReview === "PASS" ? "🟢 PASS(独立Reviewer)"
|
|
431
|
-
: presentation.missionReview === "self-rechecked" ? "🟢 自己再確認済み(修正著者・独立PASSではない)"
|
|
432
|
-
: presentation.missionReview === "evidence-gaps" ? "🟡 補足あり(非ブロッキング・PASSではない)"
|
|
433
|
-
: presentation.missionReview === "skipped-low-risk" ? "免除(低リスク)" : proof.review;
|
|
441
|
+
const proof = renderDebriefProof(result.debrief, locale);
|
|
442
|
+
const review = presentation.missionReview === "PASS" ? t("🟢 PASS(独立Reviewer)", "🟢 PASS (independent Reviewer)")
|
|
443
|
+
: presentation.missionReview === "self-rechecked" ? t("🟢 自己再確認済み(修正著者・独立PASSではない)", "🟢 Author self-rechecked (not independent PASS)")
|
|
444
|
+
: presentation.missionReview === "evidence-gaps" ? t("🟡 補足あり(非ブロッキング・PASSではない)", "🟡 Evidence gaps (non-blocking; not PASS)")
|
|
445
|
+
: presentation.missionReview === "skipped-low-risk" ? t("免除(低リスク)", "Waived (low risk)") : proof.review;
|
|
434
446
|
const estimate = result.debrief?.estimatedCost;
|
|
435
|
-
const estimatedCost = estimate === undefined ? "計測不可"
|
|
436
|
-
: estimate.pricedRequests === 0 ? "未換算"
|
|
437
|
-
: `$${estimate.usd.toFixed(4)}${estimate.unpricedRequests > 0 ? "(一部未換算)" : ""}`;
|
|
447
|
+
const estimatedCost = estimate === undefined ? t("計測不可", "Unavailable")
|
|
448
|
+
: estimate.pricedRequests === 0 ? t("未換算", "Unpriced")
|
|
449
|
+
: `$${estimate.usd.toFixed(4)}${estimate.unpricedRequests > 0 ? t("(一部未換算)", " (partly unpriced)") : ""}`;
|
|
438
450
|
const measuredTokens = result.debrief?.mixCoverage?.observedTokens;
|
|
439
451
|
const tokenText = result.cost.total_tokens.availability === "unavailable" && measuredTokens !== undefined && measuredTokens > 0
|
|
440
|
-
? `${measuredTokens.toLocaleString("
|
|
441
|
-
:
|
|
452
|
+
? `${measuredTokens.toLocaleString(locale)} ${locale === "en" && measuredTokens === 1 ? "token" : "tokens"}${t("(観測分)", " (observed)")}`
|
|
453
|
+
: metric(result.cost.total_tokens, (value) => `${value.toLocaleString(locale)} ${locale === "en" && value === 1 ? "token" : "tokens"}`);
|
|
442
454
|
const body = [
|
|
443
|
-
"🐾 SORTIE DOGS — 帰還報告",
|
|
455
|
+
t("🐾 SORTIE DOGS — 帰還報告", "🐾 SORTIE DOGS — Return Report"),
|
|
456
|
+
t("TACTICAL DESK / 作戦卓", "TACTICAL DESK / Operations"),
|
|
457
|
+
"────────────────────",
|
|
444
458
|
result.result_id[0],
|
|
445
459
|
"",
|
|
446
|
-
`${color} ${achievement} — ${
|
|
460
|
+
`${color} ${achievement} — ${display(presentation.statusSummary)}`,
|
|
447
461
|
"",
|
|
448
|
-
|
|
449
|
-
`経過 ⏱ ${metricText(result.speed.goal_wall_ms, duration)} ※待機含む`,
|
|
450
|
-
`最終達成条件 ◔ ${criteria}`,
|
|
451
|
-
`対象検証 ${proof.validation}`,
|
|
452
|
-
`SourceReview ${review}`,
|
|
453
|
-
...(presentation.reviewNote ? [`レビュー補足 ${displayText(presentation.reviewNote)}`] : []),
|
|
454
|
-
`Commit ${displayText(presentation.commit)}`,
|
|
462
|
+
...renderDebriefTraits(result.debrief, result.mission.status === "COMPLETED", locale),
|
|
455
463
|
"",
|
|
456
|
-
"
|
|
457
|
-
|
|
464
|
+
"⚑ MISSION",
|
|
465
|
+
`${t("経過", "Elapsed")} ⏱ ${metric(result.speed.goal_wall_ms, duration)} ${t("※待機含む", "※Includes waiting")}`,
|
|
466
|
+
`${t("最終達成条件", "Final criteria")} ◔ ${criteria}`,
|
|
467
|
+
`${t("対象検証", "Target validation")} ${proof.validation}`,
|
|
468
|
+
`SourceReview ${review}`,
|
|
469
|
+
...(presentation.reviewNote ? [`${t("レビュー補足", "Review note")} ${display(presentation.reviewNote)}`] : []),
|
|
470
|
+
`Commit ${display(presentation.commit)}`,
|
|
458
471
|
"",
|
|
459
|
-
"
|
|
460
|
-
|
|
472
|
+
t("🔧 実装", "🔧 Implementation"),
|
|
473
|
+
display(presentation.implementation),
|
|
461
474
|
"",
|
|
462
|
-
"
|
|
463
|
-
|
|
475
|
+
t("⏳ 未実施", "⏳ Pending"),
|
|
476
|
+
presentation.pending?.trim() ? display(presentation.pending) : t("未確認(未実施なしを確認した意味ではありません)", "Unconfirmed (does not mean nothing remains)"),
|
|
464
477
|
"",
|
|
465
478
|
"🪙 COST / PACK",
|
|
466
|
-
|
|
467
|
-
|
|
479
|
+
`${t("使用量", "Usage")} ${tokenText}`,
|
|
480
|
+
`${t("予測費用", "Estimated cost")} ${estimatedCost} ${t("※予測概算", "※Estimate only")}`,
|
|
481
|
+
`${t("host計上費用", "Host-recorded cost")} ${result.cost.cost_usd.availability === "available" ? `$${result.cost.cost_usd.value.toFixed(4)}` : t("unknown(全体の計上額未取得)", "unknown (total host cost unavailable)")}`,
|
|
482
|
+
t("※換算済み予測費用・host計上費用は請求総額ではありません。未換算・未取得を$0扱いしません", "※Priced estimates and host-recorded costs are not the total bill. Unpriced or missing amounts are not $0."),
|
|
483
|
+
"",
|
|
484
|
+
...renderDebrief(result.debrief, locale),
|
|
468
485
|
"",
|
|
469
|
-
...
|
|
486
|
+
...renderCareer(result.career, locale),
|
|
470
487
|
"",
|
|
471
|
-
...
|
|
488
|
+
...renderDebriefTraitEvidence(result.debrief, result.mission.status === "COMPLETED", locale),
|
|
472
489
|
"",
|
|
473
490
|
"🛑 STOP REASON",
|
|
474
|
-
|
|
491
|
+
display(presentation.stopReason ?? result.mission.stop_reason),
|
|
475
492
|
"",
|
|
476
|
-
"
|
|
477
|
-
|
|
478
|
-
|
|
493
|
+
"➡️ NEXT",
|
|
494
|
+
display(presentation.next),
|
|
495
|
+
...(result.mission.status === "COMPLETED" ? ["", t("└─ 記録を閉じる。次の作戦へ。", "└─ Record closed. On to the next operation.")] : []),
|
|
496
|
+
"",
|
|
497
|
+
t("※使用量は最終応答生成前の計測", "※Usage measured before final response generation"),
|
|
498
|
+
].filter(line => line !== "").map((line, index) => index === 1
|
|
499
|
+
? `\`${line}\`` : reportLiteral(line.trim())).join("\n\n");
|
|
500
|
+
return `<details>\n<summary><strong>${t("🐾 SORTIE DOGS — 帰還報告", "🐾 SORTIE DOGS — Return Report")}|${color} ${summaryLabel}</strong></summary>\n\n${body}\n\n</details>`;
|
|
479
501
|
}
|
|
480
502
|
export function formatRunMetrics(metrics) {
|
|
481
503
|
const elapsed = metrics.durationMilliseconds === undefined ? "duration unavailable" : `${duration(metrics.durationMilliseconds)} wall-clock`;
|
|
@@ -567,7 +589,7 @@ export function sanitizeTerminalReport(text) {
|
|
|
567
589
|
const keep = (block) => `\0SORTIE_PROSE_${preserved.push(block) - 1}\0`;
|
|
568
590
|
return text.replace(/<details\b[^>]*>[\s\S]*?<\/details>/giu, block => {
|
|
569
591
|
const summary = /<summary\b[^>]*>([\s\S]*?)<\/summary>/iu.exec(block)?.[1]?.replace(/<[^>]*>/gu, "").trim() ?? "";
|
|
570
|
-
return /^(?:Evidence\b|🐾\s*SORTIE DOGS\s*[—-]\s
|
|
592
|
+
return /^(?:Evidence\b|🐾\s*SORTIE DOGS\s*[—-]\s*(?:帰還報告|Return Report))/iu.test(summary) ? "" : keep(block);
|
|
571
593
|
})
|
|
572
594
|
.replace(/^[ \t]*(`{3,}|~{3,})[^\r\n]*\r?\n[\s\S]*?^[ \t]*\1[ \t]*$/gimu, (block, _fence, offset, source) => {
|
|
573
595
|
const preceding = source.slice(0, offset).trimEnd().split(/\r?\n\s*\r?\n/u).at(-1) ?? "";
|
|
@@ -591,14 +613,16 @@ export function insertRunMetrics(text, metrics) {
|
|
|
591
613
|
lines.splice(checkpoint.index + 1, 0, "", formatRunMetrics(metrics));
|
|
592
614
|
return lines.join(newline);
|
|
593
615
|
}
|
|
594
|
-
export function insertSortieResult(text, result, missionReview, reviewEvidenceGaps) {
|
|
616
|
+
export function insertSortieResult(text, result, missionReview, reviewEvidenceGaps, localeSetting) {
|
|
617
|
+
const locale = resolveReportLocale(localeSetting);
|
|
618
|
+
const t = reportText(locale);
|
|
595
619
|
const visible = sanitizeTerminalReport(text);
|
|
596
620
|
const gapSummary = missionReview === "evidence-gaps"
|
|
597
|
-
? reviewEvidenceGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").trim().slice(0, 500) || "独立Reviewに未確認事項あり"
|
|
621
|
+
? reviewEvidenceGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").trim().slice(0, 500) || t("独立Reviewに未確認事項あり", "Independent Review has unconfirmed items")
|
|
598
622
|
: undefined;
|
|
599
623
|
const reported = extractSortiePresentation(text);
|
|
600
624
|
// Preserve uncertainty visibly without manufacturing unfinished work or a required next action.
|
|
601
|
-
const presentation = { ...reported, missionReview, ...(gapSummary ? { reviewNote: gapSummary } : {}) };
|
|
625
|
+
const presentation = { ...reported, locale, missionReview, ...(gapSummary ? { reviewNote: gapSummary } : {}) };
|
|
602
626
|
const checkpoint = terminalCheckpoint(visible);
|
|
603
627
|
if (checkpoint === undefined)
|
|
604
628
|
return visible;
|
|
@@ -631,13 +655,16 @@ export function insertSortieResult(text, result, missionReview, reviewEvidenceGa
|
|
|
631
655
|
card = formatSortieResult(result, presentation);
|
|
632
656
|
}
|
|
633
657
|
catch {
|
|
634
|
-
card = "<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告</strong></summary>\n\n**確認:** 表示集計を取得できません。任務結果は先頭の状態を参照。\n\n</details>";
|
|
658
|
+
card = t("<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告</strong></summary>\n\n**確認:** 表示集計を取得できません。任務結果は先頭の状態を参照。\n\n</details>", "<details>\n<summary><strong>🐾 SORTIE DOGS — Return Report</strong></summary>\n\n**Note:** Display totals unavailable. See the leading status for the mission result.\n\n</details>");
|
|
635
659
|
}
|
|
636
660
|
cleaned.splice(checkpoint.index + 1, 0, "", card, "");
|
|
637
661
|
return cleaned.join(newline).trimEnd();
|
|
638
662
|
}
|
|
639
663
|
function extractSortiePresentation(text) {
|
|
640
|
-
|
|
664
|
+
// Old host cards and quoted examples are not caller prose. In particular, a
|
|
665
|
+
// previous locale's generated unknown values must not become retained text.
|
|
666
|
+
text = topLevelLines(text).map(({ line }) => line).join("\n");
|
|
667
|
+
const first = text.split("\n").find(line => line.trim().length > 0) ?? "";
|
|
641
668
|
const statusSummary = first.split(/\s+[—-]\s+/u).slice(1).join(" — ").trim() || undefined;
|
|
642
669
|
const section = (names) => {
|
|
643
670
|
const expression = new RegExp(`^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\p{Extended_Pictographic}\\uFE0F?[ \\t]+)?(?:\\*\\*)?(?:${names})(?:\\*\\*)?[ \\t]*:?[ \\t]*(?:\\*\\*)?[ \\t]*(.*)$`, "imu");
|
|
@@ -650,7 +677,7 @@ function extractSortiePresentation(text) {
|
|
|
650
677
|
return tail.find((line) => line.trim().length > 0 && !/^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\*\\*)?(?:変更点|実装|未実施|次|NEXT|Commit|コミット)/iu.test(line))?.trim();
|
|
651
678
|
};
|
|
652
679
|
const explicitStop = /^(?:TRUE_INTERRUPTION|TRUE_BLOCKER)[ \\t]*:[ \\t]*(.+)$/imu.exec(text)?.[1]?.trim();
|
|
653
|
-
return { statusSummary, implementation: section("
|
|
680
|
+
return { statusSummary, implementation: section("変更点|実装|Implementation|Changes"), pending: section("未実施|Pending"), next: section("次|NEXT"),
|
|
654
681
|
commit: section("Commit|コミット"), stopReason: explicitStop };
|
|
655
682
|
}
|
|
656
683
|
export function createGoalReport(result, receipt) {
|
|
@@ -668,6 +695,19 @@ export function createGoalReport(result, receipt) {
|
|
|
668
695
|
models: mix != null && mix.length <= 128 && mix.every((entry) => entry.model.length <= 512) &&
|
|
669
696
|
mix.reduce((sum, entry) => sum + entry.tokens, 0) === tokens ? mix.map(({ model, tokens }) => ({ model, tokens })) : null,
|
|
670
697
|
first_pass_eligible: result.debrief?.firstPassEligible === true, traits,
|
|
698
|
+
...(result.debrief?.rare === undefined ? {} : { rare: result.debrief.rare }),
|
|
671
699
|
...(result.debrief?.overlap === undefined ? {} : { overlap: { definition: "worker-span-union/v1",
|
|
672
700
|
worker_ms: result.debrief.overlap.workerMilliseconds, wall_ms: result.debrief.overlap.wallMilliseconds } }) };
|
|
673
701
|
}
|
|
702
|
+
/** Both terminal delivery callers use the first persisted snapshot, not a later remeasurement. */
|
|
703
|
+
export function withSavedGoalReport(result, receipt, records) {
|
|
704
|
+
const report = records.map(({ event }) => event).find(event => event.kind === "goal.reported" &&
|
|
705
|
+
event.goal_id === receipt.goal_id && validGoalReport(event.report) && event.report.terminal_key === goalFingerprint(receipt));
|
|
706
|
+
if (result.debrief === undefined)
|
|
707
|
+
return result;
|
|
708
|
+
const saved = report?.kind === "goal.reported" ? report.report : undefined;
|
|
709
|
+
return { ...result, debrief: { ...result.debrief, rare: saved?.rare,
|
|
710
|
+
...(saved === undefined ? {} : { traits: saved.traits.map(trait => trait === "pack-tactics" ? "連携作戦"
|
|
711
|
+
: trait === "recovery" ? "修正から復帰" : "一発完遂"), firstPassEligible: saved.first_pass_eligible }),
|
|
712
|
+
rareRecord: saved === undefined ? "unavailable" : saved.rare === undefined ? "legacy" : "saved" } };
|
|
713
|
+
}
|
|
@@ -45,6 +45,13 @@ export interface SortieCareer {
|
|
|
45
45
|
readonly ratio: number | null;
|
|
46
46
|
};
|
|
47
47
|
readonly titles: readonly string[];
|
|
48
|
+
readonly rare: readonly {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly earned: number;
|
|
51
|
+
readonly notEarned: number;
|
|
52
|
+
readonly unknown: number;
|
|
53
|
+
readonly legacy: number;
|
|
54
|
+
}[];
|
|
48
55
|
}
|
|
49
56
|
/** Rebuild from retained source records; no cache survives deletion, retention, or reset. */
|
|
50
57
|
export declare function summarizeCareer(histories: readonly (readonly GoalFlightEventRecord[])[], coverage: CareerCoverage): SortieCareer;
|
|
@@ -52,4 +59,4 @@ export declare function summarizeCareer(histories: readonly (readonly GoalFlight
|
|
|
52
59
|
export declare function collectCareer(directories: readonly string[], currentPath: string, current: readonly GoalFlightEventRecord[], read: (path: string) => Promise<{
|
|
53
60
|
readonly records: readonly GoalFlightEventRecord[];
|
|
54
61
|
}>, maxFiles?: number): Promise<SortieCareer>;
|
|
55
|
-
export declare function renderCareer(career: SortieCareer | undefined): string[];
|
|
62
|
+
export declare function renderCareer(career: SortieCareer | undefined, locale?: import("./report-locale.ts").ReportLocale): string[];
|
|
@@ -2,6 +2,9 @@ import { readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
4
4
|
import { validGoalReport } from "../core/goal-report.js";
|
|
5
|
+
import { RARE_RULES, rareEvaluation } from "../core/rare-awards.js";
|
|
6
|
+
import { rareDisplay } from "./sortie-debrief.js";
|
|
7
|
+
import { reportText, resolveReportLocale } from "./report-locale.js";
|
|
5
8
|
/** Rebuild from retained source records; no cache survives deletion, retention, or reset. */
|
|
6
9
|
export function summarizeCareer(histories, coverage) {
|
|
7
10
|
const unique = new Map();
|
|
@@ -23,7 +26,7 @@ export function summarizeCareer(histories, coverage) {
|
|
|
23
26
|
goal.receipt = null;
|
|
24
27
|
if (event.kind === "unit.settled")
|
|
25
28
|
goal.units.set(event.reservation_id, event.elapsed_ms);
|
|
26
|
-
if (event.kind === "goal.reported" && validGoalReport(event.report))
|
|
29
|
+
if (event.kind === "goal.reported" && validGoalReport(event.report) && !goal.reports.has(event.report.terminal_key))
|
|
27
30
|
goal.reports.set(event.report.terminal_key, event.report);
|
|
28
31
|
}
|
|
29
32
|
let completed = 0, interrupted = 0, external = 0, decision = 0, active = 0, telemetryCovered = 0, recoveries = 0;
|
|
@@ -31,6 +34,7 @@ export function summarizeCareer(histories, coverage) {
|
|
|
31
34
|
const models = new Map();
|
|
32
35
|
const overlap = { worker: 0, wall: 0, covered: 0, ratio: null };
|
|
33
36
|
let modelCovered = 0, packTactics = false;
|
|
37
|
+
const rare = RARE_RULES.map(rule => ({ id: rule.id, earned: 0, notEarned: 0, unknown: 0, legacy: 0 }));
|
|
34
38
|
for (const goal of goals.values()) {
|
|
35
39
|
const receipt = goal.receipt;
|
|
36
40
|
if (receipt === null) {
|
|
@@ -55,6 +59,22 @@ export function summarizeCareer(histories, coverage) {
|
|
|
55
59
|
workerTime.covered++;
|
|
56
60
|
}
|
|
57
61
|
const report = goal.reports.get(goalFingerprint(receipt));
|
|
62
|
+
if (receipt.status === "succeeded")
|
|
63
|
+
for (const item of rare) {
|
|
64
|
+
if (report === undefined)
|
|
65
|
+
item.unknown++;
|
|
66
|
+
else if (report.rare === undefined)
|
|
67
|
+
item.legacy++;
|
|
68
|
+
else {
|
|
69
|
+
const evaluation = rareEvaluation(report.rare, item.id);
|
|
70
|
+
if (evaluation?.status === "earned")
|
|
71
|
+
item.earned++;
|
|
72
|
+
else if (evaluation?.status === "not-earned")
|
|
73
|
+
item.notEarned++;
|
|
74
|
+
else
|
|
75
|
+
item.unknown++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
58
78
|
if (report === undefined)
|
|
59
79
|
continue;
|
|
60
80
|
telemetryCovered++;
|
|
@@ -88,7 +108,7 @@ export function summarizeCareer(histories, coverage) {
|
|
|
88
108
|
return { scope: "retained-project-goals", since: dates[0] ?? null, coverage, goals: goals.size,
|
|
89
109
|
completed, interrupted, external, decision, active, telemetryCovered, firstPass, recoveries, tokens,
|
|
90
110
|
models: [...models].sort(([a], [b]) => a.localeCompare(b)).map(([model, tokens]) => ({ model, tokens })), modelCovered,
|
|
91
|
-
workerTime, goalWall, overlap, titles: [
|
|
111
|
+
workerTime, goalWall, overlap, rare, titles: [
|
|
92
112
|
...(completed > 0 ? ["任務完遂の隊"] : []), ...(packTactics ? ["連携の隊"] : []),
|
|
93
113
|
...(recoveries > 0 ? ["復帰の隊"] : []), ...(firstPass.count > 0 ? ["一発完遂の隊"] : []),
|
|
94
114
|
] };
|
|
@@ -122,27 +142,48 @@ export async function collectCareer(directories, currentPath, current, read, max
|
|
|
122
142
|
}
|
|
123
143
|
return summarizeCareer(histories, { files: paths.size + 1, included: histories.length, unavailable, truncated: selected.length < paths.size });
|
|
124
144
|
}
|
|
125
|
-
export function renderCareer(career) {
|
|
145
|
+
export function renderCareer(career, locale = resolveReportLocale()) {
|
|
146
|
+
const t = reportText(locale);
|
|
126
147
|
if (career === undefined)
|
|
127
|
-
return ["📜 PACK RECORD", "保存履歴を取得できません"];
|
|
148
|
+
return ["📜 PACK RECORD", t("保存履歴を取得できません", "Saved history unavailable")];
|
|
128
149
|
const terminal = career.goals - career.active;
|
|
129
|
-
const
|
|
150
|
+
const missions = (count) => count === 1 ? " mission" : " missions";
|
|
151
|
+
const minutes = (metric) => metric.covered === 0 ? t("計測不可", "Unavailable") : `${(metric.sum / 60000).toFixed(1)}${t("分", " min")} ※${metric.covered}/${terminal}${t("任務", missions(terminal))}`;
|
|
130
152
|
return [
|
|
131
153
|
"📜 PACK RECORD",
|
|
132
|
-
`🏁 完了 ${career.completed}`,
|
|
133
|
-
`🟡 中断 ${career.interrupted}`,
|
|
134
|
-
`⏳ 外部待機 ${career.external}`,
|
|
135
|
-
`❓ 指示待ち ${career.decision}`,
|
|
136
|
-
`🔄 進行中 ${career.active}`,
|
|
137
|
-
`↩️ 復帰 ${career.recoveries}`,
|
|
154
|
+
`🏁 ${t("完了", "Completed")} ${career.completed}`,
|
|
155
|
+
`🟡 ${t("中断", "Interrupted")} ${career.interrupted}`,
|
|
156
|
+
`⏳ ${t("外部待機", "External wait")} ${career.external}`,
|
|
157
|
+
`❓ ${t("指示待ち", "Awaiting decision")} ${career.decision}`,
|
|
158
|
+
`🔄 ${t("進行中", "Active")} ${career.active}`,
|
|
159
|
+
`↩️ ${t("復帰", "Recoveries")} ${career.recoveries}`,
|
|
160
|
+
t("※今回ではなく保存履歴の戦績。進行中は終端記録なし(現在の稼働確認ではありません)", "※Saved history, not this mission. Active means no terminal record, not confirmation of current activity."),
|
|
161
|
+
t(`※復帰は成功任務の同一子セッション内の検証失敗→変更→同じ検証成功のみ。報告取得 ${career.telemetryCovered}/${terminal}任務`, `※Recovery requires a successful mission and same-child check failure → edit → same check success. Reports ${career.telemetryCovered}/${terminal}${missions(terminal)}`),
|
|
162
|
+
`🎯 ${t("一発完遂", "Clean Sweep")} ${career.firstPass.count}/${career.firstPass.eligible}${t("判定対象任務", ` eligible${missions(career.firstPass.eligible)}`)}`,
|
|
163
|
+
"",
|
|
164
|
+
t("🎖 PACK獲得称号(保存履歴)", "🎖 PACK titles (saved history)"),
|
|
165
|
+
...[
|
|
166
|
+
["任務完遂の隊", t("任務完遂の隊", "Mission Accomplished Pack"), t("保存された正式receiptに任務完了あり", "Formal saved receipt confirms mission completion")],
|
|
167
|
+
["連携の隊", t("連携の隊", "Formation Pack"), t("報告に異なる子セッションの稼働区間重複あり(成功称号ではありません)", "Report records overlapping child sessions (not a success title)")],
|
|
168
|
+
["復帰の隊", t("復帰の隊", "Recovery Pack"), t("成功任務に上記の限定された復帰記録あり", "Successful mission with the narrowly defined recovery above")],
|
|
169
|
+
["一発完遂の隊", t("一発完遂の隊", "Clean Sweep Pack"), t("判定対象の成功任務に再試行・再計画・検証失敗なし", "Eligible successful mission without retries, replans or failed checks")],
|
|
170
|
+
].filter(([id]) => career.titles.includes(id)).map(([, title, reason]) => `${title} — ${reason}`),
|
|
171
|
+
...(career.titles.length === 0 ? [t("判定保留 — 保存範囲で確認できた称号なし", "Pending — no titles confirmed in retained scope")] : []),
|
|
172
|
+
"",
|
|
173
|
+
t("特別記章(保存履歴・記章別/合算しません)", "Special insignia (saved history; separate, never summed)"),
|
|
174
|
+
...(career.rare.filter(item => item.earned > 0).map(item => `${rareDisplay(item.id, locale).title} — ${t(`${item.earned}任務に獲得記録あり`, `Award recorded in ${item.earned}${missions(item.earned)}`)}`)),
|
|
175
|
+
...(career.rare.every(item => item.earned === 0) ? [t("確認できた獲得記録なし(未獲得の断定ではありません)", "No confirmed award records (not a claim of never earned)")] : []),
|
|
176
|
+
...career.rare.map(item => `${rareDisplay(item.id, locale).title}${t(`の判定範囲 — 成功${career.completed}任務中 非該当${item.notEarned}・判定不能${item.unknown}・旧未判定${item.legacy}`, ` coverage — ${career.completed} successful${missions(career.completed)}: not applicable ${item.notEarned} · unknown ${item.unknown} · legacy unevaluated ${item.legacy}`)}`),
|
|
177
|
+
t("※同じ任務・現在の正式receiptにつき各記章1件。再開中の旧獲得は除外。通常称号の別名を追加実績にしません", "※One record per insignia per mission's current formal receipt. Old awards excluded while reopened. Title aliases are not extra achievements."),
|
|
138
178
|
"",
|
|
139
|
-
`🪙 累積使用量 ${career.tokens.covered === 0 ? "計測不可" : `${career.tokens.sum.toLocaleString(
|
|
140
|
-
`⏱ 累積worker ${minutes(career.workerTime)}`,
|
|
141
|
-
`🕰 累積goal ${minutes(career.goalWall)}
|
|
142
|
-
`⚡ 累積重複率 ${career.overlap.ratio === null ? "計測不可" : `${career.overlap.ratio.toFixed(2)}×`} ※${career.overlap.covered}/${terminal}
|
|
179
|
+
`🪙 ${t("累積使用量", "Cumulative usage")} ${career.tokens.covered === 0 ? t("計測不可", "Unavailable") : `${career.tokens.sum.toLocaleString(locale)} ${locale === "en" && career.tokens.sum === 1 ? "token" : "tokens"}`} ※${career.tokens.covered}/${terminal}${t("任務", missions(terminal))}`,
|
|
180
|
+
`⏱ ${t("累積worker", "Cumulative worker")} ${minutes(career.workerTime)}`,
|
|
181
|
+
`🕰 ${t("累積goal", "Cumulative goal")} ${minutes(career.goalWall)} ${t("※待機・重複含む", "※Includes waiting and overlap")}`,
|
|
182
|
+
`⚡ ${t("累積重複率", "Cumulative overlap")} ${career.overlap.ratio === null ? t("計測不可", "Unavailable") : `${career.overlap.ratio.toFixed(2)}×`} ※${career.overlap.covered}/${terminal}${t("任務・速度倍率ではありません", `${missions(terminal)}; not a speed multiplier`)}`,
|
|
143
183
|
"",
|
|
144
|
-
"📦 保存範囲",
|
|
145
|
-
`${career.since?.slice(0, 10) ?? "開始日不明"}以降 / ${career.coverage.included} of ${career.coverage.files} files${career.coverage.unavailable || career.coverage.truncated ? " ※部分集計" : ""}`,
|
|
146
|
-
"※生涯戦績ではありません",
|
|
184
|
+
t("📦 保存範囲", "📦 Retained scope"),
|
|
185
|
+
`${career.since?.slice(0, 10) ?? t("開始日不明", "Unknown start")}${t("以降", " onward")} / ${career.coverage.included} of ${career.coverage.files}${career.coverage.files === 1 ? " file" : " files"}${career.coverage.unavailable || career.coverage.truncated ? t(" ※部分集計", " ※Partial aggregation") : ""}`,
|
|
186
|
+
t("※生涯戦績ではありません", "※Not a lifetime record"),
|
|
187
|
+
t("※称号も上記保存範囲のみ。未取得・削除済み履歴は判定しません", "※Titles cover only the retained scope above. Missing or deleted history is not evaluated."),
|
|
147
188
|
];
|
|
148
189
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { GoalAcceptanceContract, GoalTerminalReceipt, GoalFlightEventRecord } from "../core/goal-bound.js";
|
|
2
|
+
import { type RareAwards } from "../core/rare-awards.ts";
|
|
3
|
+
import { type ReportLocale } from "./report-locale.ts";
|
|
4
|
+
export declare function rareDisplay(id: string, locale: ReportLocale): {
|
|
5
|
+
title: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|
|
2
8
|
type Span = {
|
|
3
9
|
start: number;
|
|
4
10
|
end: number;
|
|
@@ -80,6 +86,9 @@ export interface Debrief {
|
|
|
80
86
|
readonly notes?: readonly string[];
|
|
81
87
|
readonly traits: readonly ("連携作戦" | "修正から復帰" | "一発完遂")[];
|
|
82
88
|
readonly firstPassEligible?: boolean;
|
|
89
|
+
readonly rare?: RareAwards;
|
|
90
|
+
/** Only persisted, receipt-matched evaluations are displayed as awarded records. */
|
|
91
|
+
readonly rareRecord?: "saved" | "legacy" | "unavailable";
|
|
83
92
|
readonly overlap?: {
|
|
84
93
|
readonly workerMilliseconds: number;
|
|
85
94
|
readonly wallMilliseconds: number;
|
|
@@ -96,8 +105,13 @@ export interface Debrief {
|
|
|
96
105
|
/** Extract only bounded typed metadata during the existing host history traversal. No conversation text survives. */
|
|
97
106
|
export declare function observeDebriefSession(id: string, root: boolean, messages: readonly Record<string, unknown>[], window?: Span): DebriefSession;
|
|
98
107
|
export declare function buildDebrief(receipt: GoalTerminalReceipt, contract: GoalAcceptanceContract | null, observation: DebriefObservation | undefined, records?: readonly GoalFlightEventRecord[]): Debrief;
|
|
99
|
-
|
|
100
|
-
export declare function
|
|
108
|
+
/** Display existing observations only; titles never select an execution policy. */
|
|
109
|
+
export declare function debriefOperationTitle(debrief: Debrief | undefined, completed: boolean): string | undefined;
|
|
110
|
+
export declare function renderDebriefTraits(debrief: Debrief | undefined, completed: boolean, locale?: ReportLocale): string[];
|
|
111
|
+
/** Evidence belongs after the main result, without awarding the same fact twice. */
|
|
112
|
+
export declare function renderDebriefTraitEvidence(debrief: Debrief | undefined, completed: boolean, locale?: ReportLocale): string[];
|
|
113
|
+
export declare function renderDebrief(debrief: Debrief | undefined, locale?: ReportLocale): string[];
|
|
114
|
+
export declare function renderDebriefProof(debrief: Debrief | undefined, locale?: ReportLocale): {
|
|
101
115
|
validation: string;
|
|
102
116
|
review: string;
|
|
103
117
|
};
|
|
@@ -2,6 +2,18 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { validationDurationGuidance } from "../core/validation-budget.js";
|
|
3
3
|
import { DEDICATED_WORKER_ROLES, LUNA_FABRIC_WORKER_ROLE } from "./model-routing.js";
|
|
4
4
|
import { normalizeCommand } from "./gate.js";
|
|
5
|
+
import { evaluateRareAwards, rareEvaluation, RARE_RULES } from "../core/rare-awards.js";
|
|
6
|
+
import { reportText, resolveReportLocale } from "./report-locale.js";
|
|
7
|
+
export function rareDisplay(id, locale) {
|
|
8
|
+
const t = reportText(locale);
|
|
9
|
+
return id === "rare.clean-sweep" ? {
|
|
10
|
+
title: t("静かな一撃", "Quiet Strike"),
|
|
11
|
+
reason: t("厳密な一発完遂(既存実績の特別区分・別加算なし)", "Strict Clean Sweep (reclassified existing achievement; no extra count)"),
|
|
12
|
+
} : {
|
|
13
|
+
title: t("不屈の連携", "Unbroken Formation"),
|
|
14
|
+
reason: t("修正復帰 × 異なる子の並行稼働(因果的協働成果の証明ではありません)", "Recovery × overlapping child activity (not proof of causally successful collaboration)"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
5
17
|
const object = (value) => value !== null && typeof value === "object" ? value : undefined;
|
|
6
18
|
const timeValue = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
7
19
|
const fingerprint = (value) => createHash("sha256").update(normalizeCommand(value)).digest("hex");
|
|
@@ -287,6 +299,17 @@ export function buildDebrief(receipt, contract, observation, records) {
|
|
|
287
299
|
(event.kind === "validation.admission" && event.decision !== "ALLOW") || (event.kind === "validation.settled" && event.outcome !== "passed")) &&
|
|
288
300
|
sessions.every((session) => !session.failed && new Set(session.checks.map((check) => check.command)).size === session.checks.length))
|
|
289
301
|
traits.push("一発完遂");
|
|
302
|
+
const cleanDisqualified = validation === "FAIL" || recovered || sessions.some(session => session.failed ||
|
|
303
|
+
new Set(session.checks.map(check => check.command)).size !== session.checks.length) ||
|
|
304
|
+
new Set(tasks).size !== tasks.length || settled.some(event => event.disposition !== "succeeded" || event.result_class !== "acceptance") ||
|
|
305
|
+
events?.some(event => event.kind === "goal.replanned" || event.kind === "goal.user-continued" || event.kind === "goal.revised" ||
|
|
306
|
+
event.kind === "validation.admission" && event.decision !== "ALLOW" ||
|
|
307
|
+
event.kind === "validation.settled" && event.outcome !== "passed");
|
|
308
|
+
const clean = traits.includes("一発完遂") ? "true" : cleanDisqualified ? "false" : "unknown";
|
|
309
|
+
const recovery = traits.includes("修正から復帰") ? "true"
|
|
310
|
+
: complete && commands.size > 0 && validation === "PASS" ? "false" : "unknown";
|
|
311
|
+
const coordination = traits.includes("連携作戦") ? "true" : timingComplete ? "false" : "unknown";
|
|
312
|
+
const rare = evaluateRareAwards(receipt.status === "succeeded", clean, recovery, coordination);
|
|
290
313
|
const review = reviews.filter((entry) => !sessions.some((session) => (session.lastPossibleMutation ?? 0) > entry.at ||
|
|
291
314
|
session.mutations.some((edit) => edit.end > entry.at))).at(-1);
|
|
292
315
|
const packComplete = observation.complete && sessions.every(session => session.root || session.timingComplete || Object.keys(session.models).length > 0);
|
|
@@ -306,6 +329,7 @@ export function buildDebrief(receipt, contract, observation, records) {
|
|
|
306
329
|
usd: pricing.usd, pricedRequests: pricing.pricedRequests, unpricedRequests: pricing.unpricedRequests
|
|
307
330
|
} } : {}),
|
|
308
331
|
validation, review: review?.status ?? "未確認", reviewSource: review?.source ?? "controller", traits, firstPassEligible,
|
|
332
|
+
rare, rareRecord: "unavailable",
|
|
309
333
|
validationEfficiency,
|
|
310
334
|
notes: [...(!observation.complete ? ["host履歴の取得が一部不足"] : []),
|
|
311
335
|
...(!usageComplete ? ["モデルIDまたはusageの記録が不足"] : []),
|
|
@@ -313,8 +337,8 @@ export function buildDebrief(receipt, contract, observation, records) {
|
|
|
313
337
|
...(timingComplete ? { overlap: { workerMilliseconds: children.reduce((sum, child) => sum + unionDuration(child.spans), 0),
|
|
314
338
|
wallMilliseconds: unionDuration(spans) } } : {}) };
|
|
315
339
|
}
|
|
316
|
-
const label = (text) => text.replace(/[\r\n\t]/gu, " ")
|
|
317
|
-
const milliseconds = (value) => value === null ? "未記録"
|
|
340
|
+
const label = (text) => text.replace(/[\r\n\t]/gu, " ");
|
|
341
|
+
const milliseconds = (value, locale = "ja") => value === null ? reportText(locale)("未記録", "Unrecorded")
|
|
318
342
|
: value < 1000 ? `${value}ms` : `${(value / 1000).toFixed(2)}s`;
|
|
319
343
|
const dollars = (usd) => `$${usd.toFixed(usd > 0 && usd < 0.00005 ? 6 : 4)}`;
|
|
320
344
|
const gauge = (percent) => {
|
|
@@ -323,7 +347,63 @@ const gauge = (percent) => {
|
|
|
323
347
|
const partial = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"][remainder];
|
|
324
348
|
return `${"█".repeat(whole)}${partial}${" ".repeat(10 - whole - (remainder === 0 ? 0 : 1))}`;
|
|
325
349
|
};
|
|
326
|
-
|
|
350
|
+
/** Display existing observations only; titles never select an execution policy. */
|
|
351
|
+
export function debriefOperationTitle(debrief, completed) {
|
|
352
|
+
if (!completed)
|
|
353
|
+
return undefined;
|
|
354
|
+
const traits = debrief?.traits ?? [];
|
|
355
|
+
if (traits.includes("修正から復帰"))
|
|
356
|
+
return "立て直し屋";
|
|
357
|
+
if (traits.includes("一発完遂"))
|
|
358
|
+
return undefined; // Reclassified, never a second achievement under an alias.
|
|
359
|
+
if (traits.includes("連携作戦"))
|
|
360
|
+
return "連携上手";
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
export function renderDebriefTraits(debrief, completed, locale = resolveReportLocale()) {
|
|
364
|
+
const t = reportText(locale);
|
|
365
|
+
const traits = debrief?.traits ?? [];
|
|
366
|
+
const title = debriefOperationTitle(debrief, completed);
|
|
367
|
+
const earned = completed && debrief?.rareRecord === "saved" ? RARE_RULES.filter(rule => rareEvaluation(debrief.rare, rule.id)?.status === "earned") : [];
|
|
368
|
+
return [
|
|
369
|
+
`${t("作戦称号", "Operation title")} ${title === "立て直し屋" ? t(title, "Recovery Specialist") : title === "連携上手" ? t(title, "Formation Specialist") : earned.some(rule => rule.id === "rare.clean-sweep") ? t("特別記章へ集約", "Listed under special insignia") : completed ? t("判定保留(根拠未確認)", "Pending (evidence unconfirmed)") : t("未授与(未完了)", "Not awarded (unfinished)")}`,
|
|
370
|
+
...earned.map(rule => `${t("特別記章", "Special insignia")} ${rareDisplay(rule.id, locale).title} — ${t("獲得記録あり", "Award recorded")}`),
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
/** Evidence belongs after the main result, without awarding the same fact twice. */
|
|
374
|
+
export function renderDebriefTraitEvidence(debrief, completed, locale = resolveReportLocale()) {
|
|
375
|
+
const t = reportText(locale);
|
|
376
|
+
const traits = debrief?.traits ?? [];
|
|
377
|
+
return [
|
|
378
|
+
t("✎ 称号・観測の根拠", "✎ Title and observation evidence"),
|
|
379
|
+
...(!completed ? [t("特別記章 — 未授与(正式成功なし)", "Special insignia — not awarded (no formal success)")] : debrief?.rareRecord !== "saved"
|
|
380
|
+
? [debrief?.rareRecord === "legacy" ? t("特別記章 — 旧記録のため未判定", "Special insignia — legacy record, not evaluated") : t("特別記章 — 保存判定未取得(授与未確認)", "Special insignia — saved evaluation unavailable (award unconfirmed)")]
|
|
381
|
+
: RARE_RULES.map(rule => {
|
|
382
|
+
const evaluation = rareEvaluation(debrief.rare, rule.id);
|
|
383
|
+
const display = rareDisplay(rule.id, locale);
|
|
384
|
+
const evidence = evaluation?.evidence.map(code => {
|
|
385
|
+
if (code === "receipt.succeeded")
|
|
386
|
+
return t("正式成功", "Formal success");
|
|
387
|
+
if (code === "receipt.not-succeeded")
|
|
388
|
+
return t("正式成功なし", "No formal success");
|
|
389
|
+
const [condition, state] = code.split(".");
|
|
390
|
+
const name = condition === "clean-sweep" ? t("一発完遂", "Clean Sweep") : condition === "recovery" ? t("修正復帰", "Recovery") : t("並行稼働", "Overlap");
|
|
391
|
+
return `${name}: ${state === "true" ? t("確認", "confirmed") : state === "false" ? t("不成立確認", "confirmed false") : t("根拠不足", "insufficient evidence")}`;
|
|
392
|
+
}).join(" · ");
|
|
393
|
+
return `${display.title}${t("の条件", " conditions")} — ${display.reason} / ${evaluation?.status === "earned" ? t("成立", "Confirmed") : evaluation?.status === "not-earned" ? t("非該当", "Not applicable") : t("判定不能(根拠不足・未対応ルール含む)", "Unknown (insufficient evidence or unsupported rule)")}${evidence ? ` / ${evidence}` : ""}`;
|
|
394
|
+
})),
|
|
395
|
+
...(traits.includes("連携作戦") ? [t("🐾 連携作戦 — 異なる子セッションの稼働区間が重複(成果成功の判定ではありません)", "🐾 Pack Tactics — activity spans of different child sessions overlap (not a success judgment)")] : []),
|
|
396
|
+
...(completed && traits.includes("修正から復帰") ? [t("↩️ 修正から復帰 — 同じ子セッションで対象検証失敗→変更→同じ検証成功、任務完了", "↩️ Recovery — same child session: target check failed → edit → same check passed; mission completed")] : []),
|
|
397
|
+
...(completed && traits.includes("一発完遂") ? [t("一発完遂の条件 — 完全な対象履歴で再試行・再計画・検証失敗なし、対象検証PASS・任務完了", "Clean Sweep conditions — complete target history, no retries, replans or failed checks; target checks PASS and mission completed")] : []),
|
|
398
|
+
...(!traits.includes("連携作戦") && !(completed && traits.some(trait => trait === "修正から復帰" || trait === "一発完遂"))
|
|
399
|
+
? [t("特性の根拠未確認(未観測と該当なしは区別できません)", "Trait evidence unconfirmed (unobserved cannot be distinguished from absent)")] : []),
|
|
400
|
+
t("※今回の観測のみ。別名・組合せの二重加算なし。称号は実行方針・検証・Review・Acceptanceを変更しません", "※This observation only. No duplicate counts for aliases or combinations. Titles do not change execution policy, validation, Review or Acceptance."),
|
|
401
|
+
t("※レアは厳密条件・組合せの特別認定。出現率は未計測。再表示は再授与ではありません", "※Special recognition for strict conditions or combinations; frequency unmeasured. Redisplay is not a new award."),
|
|
402
|
+
t("※記章目的の失敗作り・追加派遣・検証省略・再実行・支出は行いません", "※Do not manufacture failures, dispatch extra tasks, skip checks, rerun work or spend money for insignia."),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
export function renderDebrief(debrief, locale = resolveReportLocale()) {
|
|
406
|
+
const t = reportText(locale);
|
|
327
407
|
const validationEfficiency = debrief?.validationEfficiency ?? {
|
|
328
408
|
completed: 0, skipped: 0, rejected: 0, redundantMilliseconds: 0, medianMilliseconds: null, p90Milliseconds: null,
|
|
329
409
|
};
|
|
@@ -353,42 +433,45 @@ export function renderDebrief(debrief) {
|
|
|
353
433
|
} } : {}),
|
|
354
434
|
...(tail.some((entry) => entry.hostCostZero) ? { hostCostZero: true } : {}) });
|
|
355
435
|
}
|
|
356
|
-
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
357
|
-
: value === "WAIVED" ? "免除" : "未記録";
|
|
358
436
|
const counts = new Map(packVisible.map((entry) => [entry.model, entry.count]));
|
|
359
437
|
return [
|
|
360
|
-
|
|
361
|
-
? "予測費用未換算(モデル別token数)" : pricedUsd === 0
|
|
362
|
-
? "予測費用比率(合計$0・算出不可)" : partiallyPriced
|
|
363
|
-
? "換算済み予測費用の比率(一部未換算)" : "予測費用比率"}`,
|
|
438
|
+
`${t("モデル内訳", "Model breakdown")} ${mix === null ? t("usage未取得", "usage unavailable") : !hasPriced
|
|
439
|
+
? t("予測費用未換算(モデル別token数)", "Estimate unpriced (tokens by model)") : pricedUsd === 0
|
|
440
|
+
? t("予測費用比率(合計$0・算出不可)", "Estimated cost shares (total $0; unavailable)") : partiallyPriced
|
|
441
|
+
? t("換算済み予測費用の比率(一部未換算)", "Priced estimate shares (partly unpriced)") : t("予測費用比率", "Estimated cost shares")}`,
|
|
364
442
|
...visible.map((entry) => {
|
|
365
443
|
const count = counts.get(entry.model);
|
|
366
444
|
const input = entry.inputCache === undefined ? 0 : entry.inputCache.uncachedInputTokens +
|
|
367
445
|
entry.inputCache.cacheReadTokens + entry.inputCache.cacheWriteTokens;
|
|
368
|
-
const cache = entry.inputCache === undefined || input === 0 ? "未取得" : `${(entry.inputCache.cacheReadTokens / input * 100).toFixed(0)}%`;
|
|
446
|
+
const cache = entry.inputCache === undefined || input === 0 ? t("未取得", "unavailable") : `${(entry.inputCache.cacheReadTokens / input * 100).toFixed(0)}%`;
|
|
369
447
|
const cost = entry.estimatedCost;
|
|
370
448
|
const share = cost?.pricedRequests
|
|
371
449
|
? pricedUsd > 0
|
|
372
|
-
? `${gauge(cost.usd / pricedUsd * 100)} ${(cost.usd / pricedUsd * 100).toFixed(1)}% · ${dollars(cost.usd)}${cost.complete ? "" : "(一部未換算)"}`
|
|
373
|
-
:
|
|
374
|
-
: "費用未換算";
|
|
375
|
-
return `🐕 ${label(entry.model)} ${share} · ${entry.tokens.toLocaleString(
|
|
450
|
+
? `${gauge(cost.usd / pricedUsd * 100)} ${(cost.usd / pricedUsd * 100).toFixed(1)}% · ${dollars(cost.usd)}${cost.complete ? "" : t("(一部未換算)", " (partly unpriced)")}`
|
|
451
|
+
: `${t("比率算出不可", "share unavailable")} · ${dollars(cost.usd)}`
|
|
452
|
+
: t("費用未換算", "unpriced");
|
|
453
|
+
return `🐕 ${entry.model === "その他" ? t("その他", "Other") : entry.model === "未分類" ? t("未分類", "Unclassified") : label(entry.model)} ${share} · ${entry.tokens.toLocaleString(locale)} ${locale === "en" && entry.tokens === 1 ? "token" : "tokens"}${count === undefined ? "" : ` ×${count}`} ↺${cache}${entry.hostCostZero ? "†" : ""}`;
|
|
376
454
|
}),
|
|
377
|
-
...(visible.some((entry) => entry.hostCostZero) ? [" †host費用0計上あり(無料・全体価格の評価ではありません)"] : []),
|
|
378
|
-
`⚡ 実行重複率 ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
455
|
+
...(visible.some((entry) => entry.hostCostZero) ? [t(" †host費用0計上あり(無料・全体価格の評価ではありません)", " †Some host costs recorded as zero (not a claim of free use or total price)")] : []),
|
|
456
|
+
`⚡ ${t("実行重複率", "Execution overlap")} ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
379
457
|
? `${(debrief.overlap.workerMilliseconds / debrief.overlap.wallMilliseconds).toFixed(2)}×`
|
|
380
|
-
: pack?.length === 0 ? "対象なし(出撃なし)" : "稼働区間の記録不足"}`,
|
|
381
|
-
" ※worker区間・速度倍率ではありません",
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
...(debrief?.notes?.length ? [
|
|
458
|
+
: pack?.length === 0 ? t("対象なし(出撃なし)", "Not applicable (no dispatches)") : t("稼働区間の記録不足", "Activity spans incomplete")}`,
|
|
459
|
+
t(" ※worker区間・速度倍率ではありません", " ※Worker spans, not a speed multiplier"),
|
|
460
|
+
`${t("検証効率", "Validation efficiency")} ${t("完了", "completed ")}${validationEfficiency.completed} · skip${validationEfficiency.skipped} · reject${validationEfficiency.rejected} · ${t("重複回避", "redundancy avoided ")}${milliseconds(validationEfficiency.redundantMilliseconds, locale)}`,
|
|
461
|
+
`${t("検証時間", "Validation time")} median ${milliseconds(validationEfficiency.medianMilliseconds, locale)} · p90 ${milliseconds(validationEfficiency.p90Milliseconds, locale)}`,
|
|
462
|
+
...(debrief?.notes?.length ? [`${t("計測範囲", "Measurement scope")} ${debrief.notes.map(note => locale === "ja" ? note : ({
|
|
463
|
+
"host履歴の取得が一部不足": "Partial host history",
|
|
464
|
+
"モデルIDまたはusageの記録が不足": "Model ID or usage records incomplete",
|
|
465
|
+
"稼働区間の時刻が不足(token集計とは独立)": "Activity timestamps incomplete (independent of token aggregation)",
|
|
466
|
+
}[note] ?? "Additional measurement limitation")).join(" · ")}`] : []),
|
|
385
467
|
];
|
|
386
468
|
}
|
|
387
|
-
export function renderDebriefProof(debrief) {
|
|
469
|
+
export function renderDebriefProof(debrief, locale = resolveReportLocale()) {
|
|
470
|
+
const t = reportText(locale);
|
|
388
471
|
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
389
|
-
: value === "WAIVED" ? "免除" : "未記録";
|
|
472
|
+
: value === "WAIVED" ? t("免除", "Waived") : t("未記録", "Unrecorded");
|
|
390
473
|
const efficiency = debrief?.validationEfficiency;
|
|
391
474
|
return { validation: `${status(debrief?.validation ?? "未確認")}${efficiency === undefined || efficiency.skipped === 0
|
|
392
|
-
? "" : `(再実行skip ${efficiency.skipped} / ${milliseconds(efficiency.redundantMilliseconds)}
|
|
393
|
-
review: `${status(debrief?.review ?? "未確認")}${debrief?.reviewSource === "reviewer" ? "(reviewer報告)" : ""}` };
|
|
475
|
+
? "" : t(`(再実行skip ${efficiency.skipped} / ${milliseconds(efficiency.redundantMilliseconds)})`, ` (rerun skipped ${efficiency.skipped} / ${milliseconds(efficiency.redundantMilliseconds)})`)}`,
|
|
476
|
+
review: `${status(debrief?.review ?? "未確認")}${debrief?.reviewSource === "reviewer" ? t("(reviewer報告)", " (reviewer report)") : ""}` };
|
|
394
477
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type RuntimeProfile } from "./core/runtime-profile.ts";
|
|
2
2
|
/** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
|
|
3
3
|
export declare const TOOL_ENVIRONMENT = ".sortie-env";
|
|
4
|
+
/** Static presentation guidance; observed values stay in ordinary tool results, not live instructions. */
|
|
5
|
+
export declare const MISSION_PRESENTATION = "## Readable progress and return reports\nUse short, meaningful headings in ordinary user-facing updates: \u2691 start, \u25CE finding, \u270E note,\n[!] obstacle, \u21A9 recovered only after recovery is observed. Keep warnings, code and machine status exact.\nUse restrained spacing, not large ASCII frames, constant military metaphors, animation or typing delays.\nReserve a small margin note or doodle for a formal completion or earned distinction, not every message.\nWhen an ordinary Mission tool result supplies work_quantity_display, copy that host-generated label into\nyour next useful user-facing progress update. No separate progress-only status call or model turn.\nThis is estimated workload for the whole request, NOT elapsed time, cost, budget or completed-unit share.\nAn estimate revision may decrease it; only formal request completion means 100%. Do not extrapolate,\nrecompute or invent it. Task metadata may be saved without being displayed by the client; Direct may\nhave no Task card. Use ordinary response text, never a fake Task or a claim that metadata proves UI delivery.\nWhen complete_mission supplies return_report, append it verbatim exactly once outside any code fence.\nThe host-authored panel and formal receipt are authoritative; titles and decoration never prove success.\nDo not regenerate metrics, hide unknowns, schedule another model turn, or repeat work just to render it.";
|
|
4
6
|
/** Task-specific environment limits qualify general dependency setup guidance. */
|
|
5
7
|
export declare const MISSION_DEPENDENCY_WORKFLOW = "Honor original dependency limits in setup and unit objectives. If the user permits installs only for\nproven-missing packages, an installed but incompatible package is not missing: do not replace it or\ndescribe it as missing compatible tooling. General registry/compatible-version guidance does not widen\nthat task-specific limit. Reuse a supplied environment; create one only when the original request permits.\nOtherwise carry out authorized dependency repair normally. If required behavior remains unverified under\nthose limits, retain in-scope work and report the limitation, not successful verification.";
|
|
6
8
|
/** Ordinary requested Git delivery uses source scope, not repository control-storage scope. */
|
|
@@ -3,6 +3,21 @@ import { STRATEGY_TRIGGERS, SOURCE_REVIEW_RISK_TAGS } from "./core/consultation.
|
|
|
3
3
|
import { SCOUT_EVIDENCE_CODES } from "./core/scout-contract.js";
|
|
4
4
|
/** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
|
|
5
5
|
export const TOOL_ENVIRONMENT = ".sortie-env";
|
|
6
|
+
/** Static presentation guidance; observed values stay in ordinary tool results, not live instructions. */
|
|
7
|
+
export const MISSION_PRESENTATION = `## Readable progress and return reports
|
|
8
|
+
Use short, meaningful headings in ordinary user-facing updates: ⚑ start, ◎ finding, ✎ note,
|
|
9
|
+
[!] obstacle, ↩ recovered only after recovery is observed. Keep warnings, code and machine status exact.
|
|
10
|
+
Use restrained spacing, not large ASCII frames, constant military metaphors, animation or typing delays.
|
|
11
|
+
Reserve a small margin note or doodle for a formal completion or earned distinction, not every message.
|
|
12
|
+
When an ordinary Mission tool result supplies work_quantity_display, copy that host-generated label into
|
|
13
|
+
your next useful user-facing progress update. No separate progress-only status call or model turn.
|
|
14
|
+
This is estimated workload for the whole request, NOT elapsed time, cost, budget or completed-unit share.
|
|
15
|
+
An estimate revision may decrease it; only formal request completion means 100%. Do not extrapolate,
|
|
16
|
+
recompute or invent it. Task metadata may be saved without being displayed by the client; Direct may
|
|
17
|
+
have no Task card. Use ordinary response text, never a fake Task or a claim that metadata proves UI delivery.
|
|
18
|
+
When complete_mission supplies return_report, append it verbatim exactly once outside any code fence.
|
|
19
|
+
The host-authored panel and formal receipt are authoritative; titles and decoration never prove success.
|
|
20
|
+
Do not regenerate metrics, hide unknowns, schedule another model turn, or repeat work just to render it.`;
|
|
6
21
|
/** Task-specific environment limits qualify general dependency setup guidance. */
|
|
7
22
|
export const MISSION_DEPENDENCY_WORKFLOW = `Honor original dependency limits in setup and unit objectives. If the user permits installs only for
|
|
8
23
|
proven-missing packages, an installed but incompatible package is not missing: do not replace it or
|
|
@@ -297,6 +312,8 @@ ${MISSION_RESULT_HOLD_POLICY}
|
|
|
297
312
|
|
|
298
313
|
${MISSION_REFLECTION_WORKFLOW}
|
|
299
314
|
|
|
315
|
+
${MISSION_PRESENTATION}
|
|
316
|
+
|
|
300
317
|
Retain 🐾 Sortie presentation. When complete_mission returns return_report, append that host-authored
|
|
301
318
|
Markdown verbatim exactly once after a concise final outcome and key checks, outside any code fence.
|
|
302
319
|
Preserve the panel's headings, metrics, unknown values and caveats; do not recalculate, regenerate it or
|
|
@@ -521,6 +538,8 @@ to change its author. Dispatch the returned Task once; Operator recommendation d
|
|
|
521
538
|
Use project-relative paths for repository read/search/shell; the native working directory is project_root.
|
|
522
539
|
Do not reconstruct or prepend the absolute workspace path. Preserve explicitly requested external paths.
|
|
523
540
|
|
|
541
|
+
${MISSION_PRESENTATION}
|
|
542
|
+
|
|
524
543
|
For release/benchmark work, retain the user's selected package/environment and record its receipt/hash.
|
|
525
544
|
A local repack and a published tarball can have different hashes; that alone does not prohibit a requested
|
|
526
545
|
local run. Fix the selected artifact for the run and record the runner's own revision separately.
|