sortie-dogs 0.14.9 → 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 +19 -6
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/main.js +22 -1
- package/dist/core/goal-bound.js +11 -4
- package/dist/core/goal-report.d.ts +2 -0
- package/dist/core/goal-report.js +3 -1
- package/dist/core/initialize.js +5 -4
- package/dist/core/project-memory.d.ts +136 -0
- package/dist/core/project-memory.js +217 -0
- 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/declared-artifacts.js +27 -2
- package/dist/plugin/frontierharness-agent-tool-registry.json +2 -2
- package/dist/plugin/gate.d.ts +2 -0
- package/dist/plugin/gate.js +55 -9
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +8 -5
- package/dist/plugin/profiled.js +99 -12
- package/dist/plugin/receipt-presentation.d.ts +1 -1
- package/dist/plugin/receipt-presentation.js +14 -5
- 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/plugin/v2.js +3 -2
- package/dist/runtime-mission-assets.d.ts +2 -0
- package/dist/runtime-mission-assets.js +25 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,9 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.
|
|
23
|
-
· [Release notes](docs/release-v0.
|
|
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
|
+
· [Execution vision](docs/v0.14.0-execution-vision.md)
|
|
24
25
|
|
|
25
26
|
[When to use](#when-to-use-sortie-dogs) · [Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
27
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -77,7 +78,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
77
78
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
78
79
|
|
|
79
80
|
```sh
|
|
80
|
-
npm install --save-dev sortie-dogs@0.
|
|
81
|
+
npm install --save-dev sortie-dogs@0.15.2
|
|
81
82
|
npx --no-install sortie-dogs codex init .
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -106,7 +107,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
106
107
|
Use **OpenCode V2**. In the target project:
|
|
107
108
|
|
|
108
109
|
```sh
|
|
109
|
-
npm install --save-dev sortie-dogs@0.
|
|
110
|
+
npm install --save-dev sortie-dogs@0.15.2
|
|
110
111
|
npx --no-install sortie-dogs init .
|
|
111
112
|
```
|
|
112
113
|
|
|
@@ -124,6 +125,18 @@ The `v010` command, profile and configuration names remain for compatibility; th
|
|
|
124
125
|
v0.10 is installed. [OpenCode setup and configuration](docs/configuration.md) ·
|
|
125
126
|
[日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md).
|
|
126
127
|
|
|
128
|
+
### Project Memory (OpenCode v010, opt-in)
|
|
129
|
+
|
|
130
|
+
With v0.15.2 installed, run `sortie-dogs memory on .` once and reload
|
|
131
|
+
the project plugin. Ordinary public turns then become pending sources; the existing
|
|
132
|
+
Operator records semantic additions, corrections, withdrawals and decisions without
|
|
133
|
+
another save request or a dedicated model call per turn. New sessions recall the
|
|
134
|
+
current project vision and constraints as well as relevant decisions and source references.
|
|
135
|
+
`sortie-dogs memory off .` stops capture and recall without deleting saved data.
|
|
136
|
+
This is not a Codex natural-capture feature, execution permission, Review PASS or proof
|
|
137
|
+
of completion. Existing reflection defaults and explicit settings are unchanged.
|
|
138
|
+
[Scope, attachment retrieval, limits and recovery](docs/v0150-project-memory.md).
|
|
139
|
+
|
|
127
140
|
## Mission workflow
|
|
128
141
|
|
|
129
142
|
```text
|
|
@@ -271,7 +284,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
271
284
|
|
|
272
285
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
273
286
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
274
|
-
The current Mission asset marker is `0.
|
|
287
|
+
The current Mission asset marker is `0.15.2-tactical-return-v1`; the Codex skill marker is
|
|
275
288
|
`0.14.6-codex-skill-v1`. Installed markers alone do not prove a running host reloaded the new version.
|
|
276
289
|
|
|
277
290
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -287,7 +300,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
287
300
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
288
301
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
289
302
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
290
|
-
- **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) ·
|
|
291
304
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
292
305
|
|
|
293
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.
|
|
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.
|
|
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";
|
package/dist/cli/main.js
CHANGED
|
@@ -28,7 +28,8 @@ const USAGE = `Usage: sortie-dogs lint <handoff.json> [<handoff.json> ...]
|
|
|
28
28
|
[--manifest <operation-manifest.json>]
|
|
29
29
|
[--changed-paths-from <file|->]
|
|
30
30
|
[--changed-path <path> ...]
|
|
31
|
-
[--format text|json] [--quiet] [--strict]
|
|
31
|
+
[--format text|json] [--quiet] [--strict]
|
|
32
|
+
Project Memory (opt-in): sortie-dogs memory on|off|status [project-root]`;
|
|
32
33
|
const INIT_USAGE = `Usage: sortie-dogs init [project-root] [--profile stable|v010]
|
|
33
34
|
sortie-dogs init --global [--profile stable|v010]
|
|
34
35
|
This beta package defaults to the v010 profile.`;
|
|
@@ -238,6 +239,24 @@ function render(output, format) {
|
|
|
238
239
|
return output.map(({ file, pointer, code, severity, message }) => `${file} ${neutralizeTextControlCharacters(pointer || "/")} ${code} ${severity} ${neutralizeTextControlCharacters(message)}\n`).join("");
|
|
239
240
|
}
|
|
240
241
|
export async function run(argv) {
|
|
242
|
+
if (argv[0] === "memory") {
|
|
243
|
+
if (!["on", "off", "status"].includes(argv[1] ?? "") || argv.length > 3 || argv[2]?.startsWith("-")) {
|
|
244
|
+
process.stderr.write("Usage: sortie-dogs memory on|off|status [project-root]\nOpt-in once per local project; off retains data and stops capture/recall.\n");
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const memoryModule = await import(__rewriteRelativeImportExtension(`../core/project-memory.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
|
|
249
|
+
const memory = new memoryModule.ProjectMemory(await memoryModule.projectMemoryFile(resolve(argv[2] ?? ".")));
|
|
250
|
+
if (argv[1] !== "status")
|
|
251
|
+
await memory.configure(argv[1] === "on");
|
|
252
|
+
process.stdout.write(JSON.stringify(await memory.recall(resolve(argv[2] ?? "."))) + "\n");
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
process.stderr.write(`Project Memory: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
241
260
|
if (argv[0] === "codex") {
|
|
242
261
|
if (argv[1] === "init") {
|
|
243
262
|
if (argv[2] === "--help" && argv.length === 3) {
|
|
@@ -354,6 +373,8 @@ export async function run(argv) {
|
|
|
354
373
|
if (initialized.preservedLegacyPaths.length > 0) {
|
|
355
374
|
process.stdout.write(`Preserved legacy runtime files: ${initialized.preservedLegacyPaths.join(", ")}.\n`);
|
|
356
375
|
}
|
|
376
|
+
if (profile === "v010")
|
|
377
|
+
process.stdout.write("Optional Project Memory: sortie-dogs memory on [project-root]. Off stops capture/recall and retains data. Reload the project plugin after first opt-in.\n");
|
|
357
378
|
const override = profile === "v010"
|
|
358
379
|
? await initializer.coordinatorModelOverride(global ? target : resolve(initArgs[0] ?? process.cwd()), global).catch(() => undefined)
|
|
359
380
|
: undefined;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -144,6 +144,9 @@ export function reduceGoalFlight(records) {
|
|
|
144
144
|
const validationAdmissions = new Map();
|
|
145
145
|
const validationSettlements = new Map();
|
|
146
146
|
const reopenedValidationEvidence = new Set();
|
|
147
|
+
// Older revisions projected the unit ceiling onto validation capacity. Replay their
|
|
148
|
+
// subsequent, stricter recorded admissions without shrinking the retained capacity.
|
|
149
|
+
let validationAdmissionLimit = null;
|
|
147
150
|
for (const [index, record] of records.entries()) {
|
|
148
151
|
requireState(record.sequence === index + 1 && record.previous_hash === previous && HASH.test(record.event_hash), "invalid", "Goal ledger chain is malformed.");
|
|
149
152
|
previous = record.event_hash;
|
|
@@ -170,6 +173,7 @@ export function reduceGoalFlight(records) {
|
|
|
170
173
|
validationAdmissions.clear();
|
|
171
174
|
validationSettlements.clear();
|
|
172
175
|
reopenedValidationEvidence.clear();
|
|
176
|
+
validationAdmissionLimit = null;
|
|
173
177
|
continue;
|
|
174
178
|
}
|
|
175
179
|
requireState(state.goal_id !== null && event.goal_id === state.goal_id, "transition", "Goal identity mismatch.");
|
|
@@ -182,14 +186,13 @@ export function reduceGoalFlight(records) {
|
|
|
182
186
|
else if (event.kind === "goal.revised") {
|
|
183
187
|
requireState(state.phase !== "terminal" && event.revision === state.revision + 1 && event.scope_epoch === state.scope_epoch + 1 &&
|
|
184
188
|
HASH.test(event.acceptance_fingerprint) && event.budget.max_units >= state.consumed_units &&
|
|
185
|
-
event.budget.max_units >= state.validation_budget.consumed &&
|
|
186
189
|
validAcceptanceContract(event.acceptance_contract) &&
|
|
187
190
|
(event.reset_no_progress === undefined || typeof event.reset_no_progress === "boolean"), "transition", "Scope revision is stale or resets consumed budget.");
|
|
188
191
|
state = { ...state, revision: event.revision, scope_epoch: event.scope_epoch,
|
|
189
192
|
acceptance_fingerprint: event.acceptance_fingerprint, latest_user_message_id: event.origin_user_message_id,
|
|
190
193
|
selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
|
|
191
194
|
validation_budget: { ...state.validation_budget,
|
|
192
|
-
limit: state.validation_budget.limit === null ? null : event.budget.max_units },
|
|
195
|
+
limit: state.validation_budget.limit === null ? null : Math.max(state.validation_budget.limit, state.validation_budget.consumed, event.budget.max_units) },
|
|
193
196
|
acceptance_contract: event.acceptance_contract,
|
|
194
197
|
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
|
|
195
198
|
receipt: null, tickets: [], ...(event.reset_no_progress === true
|
|
@@ -198,6 +201,8 @@ export function reduceGoalFlight(records) {
|
|
|
198
201
|
if (event.reset_no_progress === true)
|
|
199
202
|
replanRevision = null;
|
|
200
203
|
legacyResetRevision = event.reset_no_progress === undefined;
|
|
204
|
+
if (validationAdmissionLimit !== null)
|
|
205
|
+
validationAdmissionLimit = Math.max(event.budget.max_units, state.validation_budget.consumed);
|
|
201
206
|
}
|
|
202
207
|
else if (event.kind === "ticket.issued") {
|
|
203
208
|
requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
|
|
@@ -318,13 +323,15 @@ export function reduceGoalFlight(records) {
|
|
|
318
323
|
const latest = [...validationSettlements.values()].filter(entry => entry.evidence_key === event.evidence_key).at(-1);
|
|
319
324
|
const blocked = latest !== undefined && latest.outcome !== "passed" && !retryableValidationFailure(latest.outcome, latest.exit_code);
|
|
320
325
|
requireState(event.scope !== null && event.consumed === state.validation_budget.consumed + 1 &&
|
|
321
|
-
(
|
|
326
|
+
(validationAdmissionLimit === null || event.limit >= validationAdmissionLimit) &&
|
|
322
327
|
((!duplicate && !blocked && !inFlight && reopen === undefined) || (validReopen && !inFlight)) &&
|
|
323
328
|
!state.validation_budget.reservations.some((entry) => entry.reservation_id === event.reservation_id), "budget", "Validation admission is stale, duplicated, or exhausted.");
|
|
324
|
-
state = { ...state, validation_budget: { ...state.validation_budget, consumed: event.consumed,
|
|
329
|
+
state = { ...state, validation_budget: { ...state.validation_budget, consumed: event.consumed,
|
|
330
|
+
limit: Math.max(state.validation_budget.limit ?? event.limit, event.limit),
|
|
325
331
|
reservations: [...state.validation_budget.reservations,
|
|
326
332
|
{ reservation_id: event.reservation_id, operation_id: event.operation_id, evidence_key: event.evidence_key }] } };
|
|
327
333
|
validationAdmissions.set(event.reservation_id, event);
|
|
334
|
+
validationAdmissionLimit = event.limit;
|
|
328
335
|
if (reopen !== undefined)
|
|
329
336
|
reopenedValidationEvidence.add(event.evidence_key);
|
|
330
337
|
}
|
|
@@ -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) &&
|
package/dist/core/initialize.js
CHANGED
|
@@ -136,11 +136,12 @@ function classifyVersionTransition(installedValue, currentValue, v010Restore = f
|
|
|
136
136
|
const adjacentPreOneLine = installed.major === 0 && current.major === 0 &&
|
|
137
137
|
current.minor === installed.minor + 1;
|
|
138
138
|
// v0.11 used a different runtime. The v010 asset migration restores the 0.10
|
|
139
|
-
// preview into the 0.12–0.
|
|
139
|
+
// preview into the 0.12–0.15 Mission line without selecting a v0.11 execution path.
|
|
140
140
|
const restoredPreview = v010Restore && installed.major === 0 && installed.minor === 10 &&
|
|
141
|
-
current.major === 0 && [12, 13, 14].includes(current.minor);
|
|
142
|
-
const retainedMission = v010Restore && installed.major === 0 &&
|
|
143
|
-
current.
|
|
141
|
+
current.major === 0 && [12, 13, 14, 15].includes(current.minor);
|
|
142
|
+
const retainedMission = v010Restore && installed.major === 0 &&
|
|
143
|
+
((installed.minor === 12 && current.minor === 14) || ([12, 13].includes(installed.minor) && current.minor === 15)) &&
|
|
144
|
+
current.major === 0;
|
|
144
145
|
return sameLine || adjacentPreOneLine || restoredPreview || retainedMission ? "compatible-update" : "incompatible";
|
|
145
146
|
}
|
|
146
147
|
async function metadata(path) {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface MemorySource {
|
|
2
|
+
id: string;
|
|
3
|
+
session: string;
|
|
4
|
+
message: string;
|
|
5
|
+
actor: string;
|
|
6
|
+
text: string;
|
|
7
|
+
attachments: Array<{
|
|
8
|
+
url: string;
|
|
9
|
+
mime?: string;
|
|
10
|
+
filename?: string;
|
|
11
|
+
}>;
|
|
12
|
+
observedAt: string;
|
|
13
|
+
availability: "retained-reference";
|
|
14
|
+
}
|
|
15
|
+
export interface MemoryChange {
|
|
16
|
+
key: string;
|
|
17
|
+
scope: "project" | "workspace" | "mission";
|
|
18
|
+
context: string;
|
|
19
|
+
kind: "vision" | "constraint" | "decision" | "unresolved" | "result";
|
|
20
|
+
status: "proposed" | "adopted" | "withdrawn" | "unverified";
|
|
21
|
+
text: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
supersedes?: string;
|
|
24
|
+
consultation?: {
|
|
25
|
+
id: string;
|
|
26
|
+
callID: string;
|
|
27
|
+
disposition: "adopted" | "rejected" | "partial";
|
|
28
|
+
effect: "improved" | "unchanged" | "worsened" | "unverified";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface MemoryEvent extends MemoryChange {
|
|
32
|
+
id: string;
|
|
33
|
+
source: string;
|
|
34
|
+
operation: string;
|
|
35
|
+
actor: string;
|
|
36
|
+
at: string;
|
|
37
|
+
}
|
|
38
|
+
/** Stable repository identity uses local Git common-dir, never remote URL or a worktree path. */
|
|
39
|
+
export declare function projectMemoryFile(root: string): Promise<string>;
|
|
40
|
+
/** One atomic document commits events and processed markers together. No reflection TTL or schema reuse. */
|
|
41
|
+
export declare class ProjectMemory {
|
|
42
|
+
readonly file: string;
|
|
43
|
+
constructor(file: string);
|
|
44
|
+
private load;
|
|
45
|
+
private transaction;
|
|
46
|
+
enabled(): Promise<boolean>;
|
|
47
|
+
source(id: string): Promise<MemorySource | undefined>;
|
|
48
|
+
sourceForModel(id: string): Promise<{
|
|
49
|
+
text: string;
|
|
50
|
+
text_truncated: boolean;
|
|
51
|
+
attachments: {
|
|
52
|
+
url: string | undefined;
|
|
53
|
+
url_omitted: boolean;
|
|
54
|
+
filename?: string | undefined;
|
|
55
|
+
mime?: string | undefined;
|
|
56
|
+
source: string;
|
|
57
|
+
index: number;
|
|
58
|
+
}[];
|
|
59
|
+
id: string;
|
|
60
|
+
session: string;
|
|
61
|
+
message: string;
|
|
62
|
+
actor: string;
|
|
63
|
+
observedAt: string;
|
|
64
|
+
availability: "retained-reference";
|
|
65
|
+
} | undefined>;
|
|
66
|
+
/** Materialize inline bytes only on demand; recall must never inject them as model text. */
|
|
67
|
+
attachment(id: string, index: number): Promise<{
|
|
68
|
+
source: string;
|
|
69
|
+
index: number;
|
|
70
|
+
url: string;
|
|
71
|
+
mime?: string;
|
|
72
|
+
filename?: string;
|
|
73
|
+
} | {
|
|
74
|
+
filename?: string | undefined;
|
|
75
|
+
source: string;
|
|
76
|
+
index: number;
|
|
77
|
+
path: string;
|
|
78
|
+
mime: string;
|
|
79
|
+
} | undefined>;
|
|
80
|
+
configure(enabled: boolean): Promise<void>;
|
|
81
|
+
capture(source: Omit<MemorySource, "observedAt" | "availability">): Promise<void>;
|
|
82
|
+
apply(source: string, operation: string, actor: string, changes: MemoryChange[]): Promise<string>;
|
|
83
|
+
recall(workspace: string, mission?: string, query?: string): Promise<{
|
|
84
|
+
enabled: false;
|
|
85
|
+
snapshot?: undefined;
|
|
86
|
+
protected_omitted?: undefined;
|
|
87
|
+
pending?: undefined;
|
|
88
|
+
pending_count?: undefined;
|
|
89
|
+
oldest_pending?: undefined;
|
|
90
|
+
last_success?: undefined;
|
|
91
|
+
sources?: undefined;
|
|
92
|
+
} | {
|
|
93
|
+
enabled: true;
|
|
94
|
+
snapshot: MemoryEvent[];
|
|
95
|
+
protected_omitted: number;
|
|
96
|
+
pending: {
|
|
97
|
+
text: string;
|
|
98
|
+
text_truncated: boolean;
|
|
99
|
+
attachments: {
|
|
100
|
+
url: string | undefined;
|
|
101
|
+
url_omitted: boolean;
|
|
102
|
+
filename?: string | undefined;
|
|
103
|
+
mime?: string | undefined;
|
|
104
|
+
source: string;
|
|
105
|
+
index: number;
|
|
106
|
+
}[];
|
|
107
|
+
id: string;
|
|
108
|
+
session: string;
|
|
109
|
+
message: string;
|
|
110
|
+
actor: string;
|
|
111
|
+
observedAt: string;
|
|
112
|
+
availability: "retained-reference";
|
|
113
|
+
}[];
|
|
114
|
+
pending_count: number;
|
|
115
|
+
oldest_pending: string;
|
|
116
|
+
last_success: string | null;
|
|
117
|
+
sources: {
|
|
118
|
+
text: string;
|
|
119
|
+
text_truncated: boolean;
|
|
120
|
+
attachments: {
|
|
121
|
+
url: string | undefined;
|
|
122
|
+
url_omitted: boolean;
|
|
123
|
+
filename?: string | undefined;
|
|
124
|
+
mime?: string | undefined;
|
|
125
|
+
source: string;
|
|
126
|
+
index: number;
|
|
127
|
+
}[];
|
|
128
|
+
id: string;
|
|
129
|
+
session: string;
|
|
130
|
+
message: string;
|
|
131
|
+
actor: string;
|
|
132
|
+
observedAt: string;
|
|
133
|
+
availability: "retained-reference";
|
|
134
|
+
}[];
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { durableScopeRoot } from "./durable-scope-root.js";
|
|
5
|
+
import { configRoot } from "../reflection/config.js";
|
|
6
|
+
const empty = () => ({ v: 1, enabled: false, sources: [], events: [], processed: {} });
|
|
7
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
8
|
+
const text = (value, max = 8000) => typeof value === "string" && value.trim().length > 0 && value.length <= max;
|
|
9
|
+
const sourceView = (source, fullText = false) => ({ ...source,
|
|
10
|
+
text: fullText ? source.text : source.text.slice(0, 2000), text_truncated: !fullText && source.text.length > 2000,
|
|
11
|
+
attachments: source.attachments.map((file, index) => ({ source: source.id, index,
|
|
12
|
+
...(file.mime ? { mime: file.mime.slice(0, 256) } : {}), ...(file.filename ? { filename: file.filename.slice(0, 256) } : {}),
|
|
13
|
+
url: !file.url.startsWith("data:") && file.url.length <= 2048 ? file.url : undefined,
|
|
14
|
+
url_omitted: file.url.startsWith("data:") || file.url.length > 2048 })) });
|
|
15
|
+
/** Stable repository identity uses local Git common-dir, never remote URL or a worktree path. */
|
|
16
|
+
export async function projectMemoryFile(root) {
|
|
17
|
+
// The lease suffix need not exist yet; canonicalize its existing ancestors
|
|
18
|
+
// without creating metadata merely to compute the memory identity.
|
|
19
|
+
const canonical = async (path) => realpath(path).catch(async (error) => {
|
|
20
|
+
if (error.code !== "ENOENT" || dirname(path) === path)
|
|
21
|
+
throw error;
|
|
22
|
+
return join(await canonical(dirname(path)), basename(path));
|
|
23
|
+
});
|
|
24
|
+
const identity = await canonical(await durableScopeRoot(root) ?? resolve(root));
|
|
25
|
+
return join(configRoot(), "sortie-dogs", "project-memory", `${hash(identity)}.json`);
|
|
26
|
+
}
|
|
27
|
+
/** One atomic document commits events and processed markers together. No reflection TTL or schema reuse. */
|
|
28
|
+
export class ProjectMemory {
|
|
29
|
+
file;
|
|
30
|
+
constructor(file) { this.file = file; }
|
|
31
|
+
async load() {
|
|
32
|
+
try {
|
|
33
|
+
const state = JSON.parse(await readFile(this.file, "utf8"));
|
|
34
|
+
if (state.v !== 1 || typeof state.enabled !== "boolean" || !Array.isArray(state.sources) ||
|
|
35
|
+
!Array.isArray(state.events) || !state.processed || typeof state.processed !== "object" || Array.isArray(state.processed) ||
|
|
36
|
+
!state.sources.every(source => source && text(source.id) && text(source.session) && text(source.message) && text(source.actor) &&
|
|
37
|
+
typeof source.text === "string" && Array.isArray(source.attachments) && source.attachments.every(file => file && text(file.url, Number.MAX_SAFE_INTEGER)) &&
|
|
38
|
+
typeof source.observedAt === "string" && source.availability === "retained-reference") ||
|
|
39
|
+
!state.events.every(event => event && text(event.id) && text(event.key) && text(event.context) && text(event.text) && text(event.reason) &&
|
|
40
|
+
text(event.source) && text(event.actor) && text(event.operation) && typeof event.at === "string" &&
|
|
41
|
+
["project", "workspace", "mission"].includes(event.scope) && ["vision", "constraint", "decision", "unresolved", "result"].includes(event.kind) &&
|
|
42
|
+
["proposed", "adopted", "withdrawn", "unverified"].includes(event.status)) ||
|
|
43
|
+
!Object.values(state.processed).every(item => item && text(item.operation) && text(item.fingerprint) && typeof item.at === "string"))
|
|
44
|
+
throw new Error("project-memory-invalid-state");
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === "ENOENT")
|
|
49
|
+
return empty();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async transaction(fn) {
|
|
54
|
+
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
55
|
+
const lock = `${this.file}.lock`;
|
|
56
|
+
let handle;
|
|
57
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
handle = await open(lock, "wx", 0o600);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== "EEXIST")
|
|
64
|
+
throw error;
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!handle)
|
|
69
|
+
throw new Error(`project-memory-busy: retry; if interrupted owner stopped, remove ${lock}`);
|
|
70
|
+
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
71
|
+
try {
|
|
72
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
73
|
+
const state = await this.load(), result = fn(state);
|
|
74
|
+
const output = await open(temp, "wx", 0o600);
|
|
75
|
+
try {
|
|
76
|
+
await output.writeFile(JSON.stringify(state));
|
|
77
|
+
await output.sync();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await output.close();
|
|
81
|
+
}
|
|
82
|
+
await rename(temp, this.file);
|
|
83
|
+
const directory = await open(dirname(this.file), "r").catch(() => undefined);
|
|
84
|
+
try {
|
|
85
|
+
await directory?.sync();
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await directory?.close();
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await unlink(temp).catch(() => undefined);
|
|
94
|
+
await handle.close();
|
|
95
|
+
await unlink(lock);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async enabled() { return (await this.load()).enabled; }
|
|
99
|
+
async source(id) {
|
|
100
|
+
const state = await this.load();
|
|
101
|
+
return state.enabled ? state.sources.find(source => source.id === id) : undefined;
|
|
102
|
+
}
|
|
103
|
+
async sourceForModel(id) {
|
|
104
|
+
const source = await this.source(id);
|
|
105
|
+
return source ? sourceView(source, true) : undefined;
|
|
106
|
+
}
|
|
107
|
+
/** Materialize inline bytes only on demand; recall must never inject them as model text. */
|
|
108
|
+
async attachment(id, index) {
|
|
109
|
+
if (!Number.isSafeInteger(index) || index < 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
const source = await this.source(id), file = source?.attachments[index];
|
|
112
|
+
if (!file)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (!file.url.startsWith("data:"))
|
|
115
|
+
return { ...file, source: id, index };
|
|
116
|
+
const response = await fetch(file.url);
|
|
117
|
+
if (!response.ok)
|
|
118
|
+
throw new Error("project-memory-attachment-unavailable");
|
|
119
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
120
|
+
const mime = file.mime ?? response.headers.get("content-type") ?? "application/octet-stream";
|
|
121
|
+
const extension = { "image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp",
|
|
122
|
+
"application/pdf": ".pdf", "text/plain": ".txt" }[mime] ?? ".bin";
|
|
123
|
+
const directory = `${this.file}.attachments`, path = join(directory, `${hash(file.url)}${extension}`);
|
|
124
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
125
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
126
|
+
try {
|
|
127
|
+
await writeFile(temporary, bytes, { flag: "wx", mode: 0o600 });
|
|
128
|
+
await rename(temporary, path);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await unlink(temporary).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
return { source: id, index, path, mime, ...(file.filename ? { filename: file.filename } : {}) };
|
|
134
|
+
}
|
|
135
|
+
async configure(enabled) { await this.transaction(state => { state.enabled = enabled; }); }
|
|
136
|
+
async capture(source) {
|
|
137
|
+
if (!await this.enabled())
|
|
138
|
+
return;
|
|
139
|
+
if (!text(source.id) || !text(source.session) || !text(source.message))
|
|
140
|
+
throw new Error("project-memory-invalid-source");
|
|
141
|
+
await this.transaction(state => {
|
|
142
|
+
if (!state.enabled)
|
|
143
|
+
return;
|
|
144
|
+
const previous = state.sources.find(item => item.id === source.id);
|
|
145
|
+
if (previous) {
|
|
146
|
+
if (JSON.stringify({ ...source }) !== JSON.stringify({ id: previous.id, session: previous.session, message: previous.message,
|
|
147
|
+
actor: previous.actor, text: previous.text, attachments: previous.attachments }))
|
|
148
|
+
throw new Error("project-memory-source-conflict");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
state.sources.push({ ...source, observedAt: new Date().toISOString(), availability: "retained-reference" });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async apply(source, operation, actor, changes) {
|
|
155
|
+
if (!text(operation, 256) || !text(actor, 256) || !Array.isArray(changes) || changes.length > 32)
|
|
156
|
+
throw new Error("project-memory-invalid-operation");
|
|
157
|
+
return this.transaction(state => {
|
|
158
|
+
if (!state.enabled)
|
|
159
|
+
throw new Error("project-memory-off");
|
|
160
|
+
if (!state.sources.some(item => item.id === source))
|
|
161
|
+
throw new Error("project-memory-source-not-found");
|
|
162
|
+
const fingerprint = hash(JSON.stringify({ operation, actor, changes }));
|
|
163
|
+
const previous = Object.hasOwn(state.processed, source) ? state.processed[source] : undefined;
|
|
164
|
+
if (previous) {
|
|
165
|
+
if (previous.fingerprint !== fingerprint)
|
|
166
|
+
throw new Error("project-memory-operation-conflict");
|
|
167
|
+
return "already-processed";
|
|
168
|
+
}
|
|
169
|
+
if (Object.values(state.processed).some(item => item.operation === operation))
|
|
170
|
+
throw new Error("project-memory-operation-conflict");
|
|
171
|
+
for (const change of changes) {
|
|
172
|
+
if (!text(change.key, 256) || !text(change.context, 1024) || !text(change.text) || !text(change.reason) ||
|
|
173
|
+
!["project", "workspace", "mission"].includes(change.scope) ||
|
|
174
|
+
!["vision", "constraint", "decision", "unresolved", "result"].includes(change.kind) ||
|
|
175
|
+
!["proposed", "adopted", "withdrawn", "unverified"].includes(change.status))
|
|
176
|
+
throw new Error("project-memory-invalid-change");
|
|
177
|
+
const current = state.events.filter(item => item.key === change.key && item.scope === change.scope && item.context === change.context).at(-1);
|
|
178
|
+
if ((current?.id ?? undefined) !== change.supersedes)
|
|
179
|
+
throw new Error("project-memory-snapshot-conflict");
|
|
180
|
+
if (change.consultation && (!text(change.consultation.id) || !text(change.consultation.callID) ||
|
|
181
|
+
!["adopted", "rejected", "partial"].includes(change.consultation.disposition) ||
|
|
182
|
+
!["improved", "unchanged", "worsened", "unverified"].includes(change.consultation.effect)))
|
|
183
|
+
throw new Error("project-memory-invalid-consultation");
|
|
184
|
+
state.events.push({ ...change, id: randomUUID(), source, operation, actor, at: new Date().toISOString() });
|
|
185
|
+
}
|
|
186
|
+
Object.defineProperty(state.processed, source, { value: { operation, fingerprint, at: new Date().toISOString() }, enumerable: true, configurable: true, writable: true });
|
|
187
|
+
return changes.length ? "applied" : "no-change";
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async recall(workspace, mission, query = "") {
|
|
191
|
+
const state = await this.load();
|
|
192
|
+
if (!state.enabled)
|
|
193
|
+
return { enabled: false };
|
|
194
|
+
const current = new Map();
|
|
195
|
+
for (const event of state.events)
|
|
196
|
+
current.set(JSON.stringify([event.scope, event.context, event.key]), event);
|
|
197
|
+
const snapshot = [...current.values()].filter(event => (event.scope === "project" ||
|
|
198
|
+
event.scope === "workspace" && event.context === workspace || event.scope === "mission" && event.context === mission));
|
|
199
|
+
const terms = query.toLocaleLowerCase().split(/\s+/u).filter(Boolean);
|
|
200
|
+
const protectedCondition = (event) => event.scope === "project" && event.status === "adopted" &&
|
|
201
|
+
(event.kind === "vision" || event.kind === "constraint");
|
|
202
|
+
// Preserve the current adopted purpose on ordinary resume, not obsolete or
|
|
203
|
+
// merely proposed guidance. Explicit searches retain their relevance ordering.
|
|
204
|
+
snapshot.sort((a, b) => (terms.length === 0 ? Number(protectedCondition(b)) - Number(protectedCondition(a)) :
|
|
205
|
+
terms.filter(term => b.text.toLocaleLowerCase().includes(term)).length -
|
|
206
|
+
terms.filter(term => a.text.toLocaleLowerCase().includes(term)).length) || b.at.localeCompare(a.at));
|
|
207
|
+
const selected = snapshot.slice(0, 24);
|
|
208
|
+
const pending = state.sources.filter(source => !Object.hasOwn(state.processed, source.id));
|
|
209
|
+
const excerpt = (source) => sourceView(source);
|
|
210
|
+
return { enabled: true, snapshot: selected,
|
|
211
|
+
protected_omitted: snapshot.filter(protectedCondition).length - selected.filter(protectedCondition).length,
|
|
212
|
+
pending: pending.slice(0, 8).map(excerpt),
|
|
213
|
+
pending_count: pending.length, oldest_pending: pending[0]?.observedAt ?? null,
|
|
214
|
+
last_success: Object.values(state.processed).map(item => item.at).sort().at(-1) ?? null,
|
|
215
|
+
sources: state.sources.filter(source => selected.some(event => event.source === source.id)).slice(-24).map(excerpt) };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -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;
|