@gtrabanco/pi-agentic-workflow 0.30.0 → 0.31.0
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 +41 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/merge.js +5 -0
- package/dist/config/schema.d.ts +0 -5
- package/dist/config/schema.js +31 -1
- package/dist/config/types.d.ts +15 -0
- package/dist/extension/engram.d.ts +119 -0
- package/dist/extension/engram.js +187 -0
- package/dist/extension/index.js +70 -3
- package/dist/settings/console.js +68 -22
- package/package.json +1 -1
- package/skills/execute-phase/SKILL.md +11 -2
- package/skills/init-workspace/SKILL.md +13 -2
- package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +11 -5
- package/skills/init-workspace/references/ENGRAM.md +107 -0
- package/skills/log-session/SKILL.md +17 -1
- package/skills/unit-lane/SKILL.md +17 -1
- package/skills/unit-lane/references/MEMORY.md +179 -0
package/README.md
CHANGED
|
@@ -213,6 +213,47 @@ notification. `requirements` is keyed by phase state (`pre-freeze`, `post-freeze
|
|
|
213
213
|
matching `path-protection-records@1` `justification` row in the unit's
|
|
214
214
|
`decisions.md`.
|
|
215
215
|
|
|
216
|
+
## Memory substrate — Engram (optional, ask-first)
|
|
217
|
+
|
|
218
|
+
The workflow's units save and recall their decisions through Engram, the memory
|
|
219
|
+
layer its skills document. This package can register Engram's MCP server for the
|
|
220
|
+
session, and it does so **only when you say so**:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"memory": { "engram": true }
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Registration goes through Pi's own `registerMcpServer`, which is
|
|
229
|
+
**session-scoped and not persisted** — the package registers on every session
|
|
230
|
+
start, so nothing outlives the session and nothing is written to your `mcp.json`
|
|
231
|
+
behind your back. Five rules decide the outcome, and the package reports it
|
|
232
|
+
verbatim:
|
|
233
|
+
|
|
234
|
+
| Situation | What happens |
|
|
235
|
+
|---|---|
|
|
236
|
+
| no `memory.engram` opt-in (the shipped default is `false`) | nothing is registered and nothing is said (the opt-out stays quiet) |
|
|
237
|
+
| no readable opt-in — the file exists but does not validate | nothing is registered; you are told the config is invalid, because unreadable consent is never read as a refusal you made |
|
|
238
|
+
| no `engram` binary on `PATH` | nothing is registered; you are told the install route |
|
|
239
|
+
| you already declared `engram` in a `mcp.json` Pi reads — your user file, or `<cwd>/.pi/mcp.json` in a trusted project | nothing is registered — your declaration takes precedence |
|
|
240
|
+
| binary present, opted in | the server is registered and the notification states exactly what was written |
|
|
241
|
+
|
|
242
|
+
Install Engram with:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
pi install npm:gentle-engram@latest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
That is the only documented route on purpose: the CLI's own setup subcommand
|
|
249
|
+
pins whatever version ran it and keeps serving that version until it is run
|
|
250
|
+
again, so an updated binary can sit unused behind a stale registration.
|
|
251
|
+
|
|
252
|
+
An older Pi host whose `ExtensionAPI` has no `registerMcpServer` is reported
|
|
253
|
+
instead of failing the session. The workflow-side protocol — when units save,
|
|
254
|
+
the recall order, the degraded path — lives in `unit-lane`'s memory reference,
|
|
255
|
+
not here.
|
|
256
|
+
|
|
216
257
|
## When a configured model is unavailable
|
|
217
258
|
|
|
218
259
|
Default: the command **refuses to start** and tells you why — the model is not in
|
package/dist/config/defaults.js
CHANGED
|
@@ -17,6 +17,9 @@ export const DEFAULT_CONFIG = {
|
|
|
17
17
|
// Feature 63 — model profiles: recommended built-in profiles, empty
|
|
18
18
|
// named profiles, empty order (default-only), and default demotion policy.
|
|
19
19
|
recommendedModels: true,
|
|
20
|
+
// Feature 66 — the memory substrate is ask-first: nothing is provisioned
|
|
21
|
+
// until the operator opts in.
|
|
22
|
+
memory: { engram: false },
|
|
20
23
|
profiles: {},
|
|
21
24
|
profileOrder: [],
|
|
22
25
|
profileFallback: { ...DEFAULT_PROFILE_FALLBACK },
|
package/dist/config/merge.js
CHANGED
|
@@ -127,6 +127,11 @@ export function mergeConfigs(globalFile = {}, projectFile = {}) {
|
|
|
127
127
|
pathProtection: intersectPathPolicy(SHIPPED_PATH_POLICY, mergePathProtectionOverrides(globalFile.pathProtection, projectFile.pathProtection)),
|
|
128
128
|
// Feature 63 — model profiles.
|
|
129
129
|
recommendedModels: pick(true, projectFile.recommendedModels, globalFile.recommendedModels),
|
|
130
|
+
// Feature 66 — the consent switch resolves like any other boolean knob:
|
|
131
|
+
// project wins, then global, then the shipped "no".
|
|
132
|
+
memory: {
|
|
133
|
+
engram: pick(false, projectFile.memory?.engram, globalFile.memory?.engram) === true,
|
|
134
|
+
},
|
|
130
135
|
profiles: mergeProfiles(globalFile.profiles, projectFile.profiles),
|
|
131
136
|
profileOrder: pick([], projectFile.profileOrder, globalFile.profileOrder),
|
|
132
137
|
profileFallback: mergeProfileFallback(globalFile.profileFallback, projectFile.profileFallback),
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -13,9 +13,4 @@ export type ParseResult = {
|
|
|
13
13
|
ok: false;
|
|
14
14
|
issues: ConfigIssue[];
|
|
15
15
|
};
|
|
16
|
-
/**
|
|
17
|
-
* Parse and validate one config file's text. A blank file is an empty config so
|
|
18
|
-
* the loader can treat "nothing declared" like "nothing present" without a
|
|
19
|
-
* second read.
|
|
20
|
-
*/
|
|
21
16
|
export declare function parseConfigFile(text: string): ParseResult;
|
package/dist/config/schema.js
CHANGED
|
@@ -20,6 +20,7 @@ const ROOT_KEYS = new Set([
|
|
|
20
20
|
"profiles",
|
|
21
21
|
"profileOrder",
|
|
22
22
|
"profileFallback",
|
|
23
|
+
"memory",
|
|
23
24
|
]);
|
|
24
25
|
const ROUTE_KEYS = new Set(["model", "thinking"]);
|
|
25
26
|
const COMMAND_NAME = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
@@ -331,7 +332,7 @@ function validateConfig(value) {
|
|
|
331
332
|
if (!ROOT_KEYS.has(key)) {
|
|
332
333
|
issues.push({
|
|
333
334
|
path: `$.${displayKey(key)}`,
|
|
334
|
-
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance, recommendedModels, profiles, profileOrder, profileFallback)`,
|
|
335
|
+
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance, recommendedModels, profiles, profileOrder, profileFallback, memory)`,
|
|
335
336
|
});
|
|
336
337
|
}
|
|
337
338
|
}
|
|
@@ -401,6 +402,11 @@ function validateConfig(value) {
|
|
|
401
402
|
config.recommendedModels = value.recommendedModels;
|
|
402
403
|
}
|
|
403
404
|
}
|
|
405
|
+
if (value.memory !== undefined) {
|
|
406
|
+
const memory = checkMemory(value.memory, "$.memory", issues);
|
|
407
|
+
if (memory)
|
|
408
|
+
config.memory = memory;
|
|
409
|
+
}
|
|
404
410
|
if (value.profiles !== undefined) {
|
|
405
411
|
if (!isRecord(value.profiles)) {
|
|
406
412
|
issues.push({ path: "$.profiles", message: "must be an object mapping profile names to profile definitions" });
|
|
@@ -436,6 +442,30 @@ function validateConfig(value) {
|
|
|
436
442
|
* the loader can treat "nothing declared" like "nothing present" without a
|
|
437
443
|
* second read.
|
|
438
444
|
*/
|
|
445
|
+
/** Strict validator for the `memory` consent block (feature 66, AC3). */
|
|
446
|
+
function checkMemory(value, path, issues) {
|
|
447
|
+
if (!isRecord(value)) {
|
|
448
|
+
issues.push({ path, message: `must be a JSON object, got ${describe(value)}` });
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
let unknown = false;
|
|
452
|
+
for (const key of Object.keys(value)) {
|
|
453
|
+
if (key !== "engram") {
|
|
454
|
+
unknown = true;
|
|
455
|
+
issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown memory key "${key}" (allowed: engram)` });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const out = {};
|
|
459
|
+
if (value.engram !== undefined) {
|
|
460
|
+
if (typeof value.engram !== "boolean") {
|
|
461
|
+
issues.push({ path: `${path}.engram`, message: `must be a boolean, got ${describe(value.engram)}` });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
out.engram = value.engram;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return unknown ? undefined : out;
|
|
468
|
+
}
|
|
439
469
|
export function parseConfigFile(text) {
|
|
440
470
|
if (text.trim() === "")
|
|
441
471
|
return { ok: true, config: {} };
|
package/dist/config/types.d.ts
CHANGED
|
@@ -75,6 +75,9 @@ export interface ConfigFile {
|
|
|
75
75
|
/** When true (default), the built-in provider profiles are included in the
|
|
76
76
|
* resolution chain (feature 63, AC1). */
|
|
77
77
|
recommendedModels?: boolean;
|
|
78
|
+
/** Feature 66 (AC3/D3): the memory-substrate consent surface — Engram's MCP
|
|
79
|
+
* registration is ask-first, so nothing is provisioned unless this says so. */
|
|
80
|
+
memory?: MemoryConfigFile;
|
|
78
81
|
/** Named user profiles keyed by profile name (feature 63, AC5). */
|
|
79
82
|
profiles?: Record<string, ProfileFile>;
|
|
80
83
|
/** Ordered list of profile names to probe; empty means ["default"] only
|
|
@@ -92,6 +95,16 @@ export type UnavailableRoutePolicy = "stop" | "inherit";
|
|
|
92
95
|
*/
|
|
93
96
|
export type SettlePolicy = "keep" | "restore";
|
|
94
97
|
/** One rejected field inside one config file, addressed by a JSON-path-ish string. */
|
|
98
|
+
/** What an operator WRITES under `memory` (feature 66). */
|
|
99
|
+
export interface MemoryConfigFile {
|
|
100
|
+
/** Opt in to the package registering the Engram MCP server when its binary
|
|
101
|
+
* resolves. Absent or `false` registers nothing. */
|
|
102
|
+
engram?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/** What the extension READS: every `memory` knob resolved (feature 66). */
|
|
105
|
+
export interface MemoryConfig {
|
|
106
|
+
engram: boolean;
|
|
107
|
+
}
|
|
95
108
|
export interface ConfigIssue {
|
|
96
109
|
path: string;
|
|
97
110
|
message: string;
|
|
@@ -118,6 +131,8 @@ export interface EffectiveConfig {
|
|
|
118
131
|
profileOrder: string[];
|
|
119
132
|
/** The effective profile-fallback demotion policy (feature 63). */
|
|
120
133
|
profileFallback: ProfileFallbackConfig;
|
|
134
|
+
/** Feature 66: the memory-substrate consent surface. */
|
|
135
|
+
memory: MemoryConfig;
|
|
121
136
|
/** Feature 63: declaredness of the implicit `default` profile. */
|
|
122
137
|
declared: {
|
|
123
138
|
default: RouteDeclaration;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engram MCP provisioning (feature 66, P4 — AC3, decision D3).
|
|
3
|
+
*
|
|
4
|
+
* The memory substrate's registration is **ask-first and reversible**, which is
|
|
5
|
+
* why it goes through Pi's own consent surface instead of writing a
|
|
6
|
+
* machine-global `mcp.json` behind the operator's back:
|
|
7
|
+
*
|
|
8
|
+
* - `pi.registerMcpServer(name, config)` is session-scoped and NOT persisted
|
|
9
|
+
* (Pi's contract), so the package registers on every session start and nothing
|
|
10
|
+
* outlives the session unless the operator asks for it.
|
|
11
|
+
* - Registration happens only when the `engram` binary actually resolves — a
|
|
12
|
+
* registered-but-broken server would surface as an extension error on every
|
|
13
|
+
* session, which is a worse default than not registering.
|
|
14
|
+
* - A server the operator already declared in an `mcp.json` Pi reads — the
|
|
15
|
+
* user file or, for a trusted project, `<cwd>/.pi/mcp.json` — wins (Pi gives
|
|
16
|
+
* it precedence), so the package reports that and does not shadow it.
|
|
17
|
+
* - The operator's `memory.engram` config key is the consent switch: absent or
|
|
18
|
+
* `false` registers nothing. This module never provisions without it.
|
|
19
|
+
*
|
|
20
|
+
* Every outcome is reported verbatim — "exactly what was written" is the rule
|
|
21
|
+
* the sibling `init-workspace` consent flow already follows, and a
|
|
22
|
+
* provisioning step the operator cannot read back is one they cannot trust.
|
|
23
|
+
*
|
|
24
|
+
* The install route is `pi install npm:gentle-engram@latest`. The CLI's own
|
|
25
|
+
* setup subcommand is deliberately NOT the documented route: it pins whatever
|
|
26
|
+
* version it ran with and keeps serving it until it is run again, so an updated
|
|
27
|
+
* binary can sit unused behind a stale registration.
|
|
28
|
+
*
|
|
29
|
+
* Pi-free by construction — the binary lookup, the environment, the "already
|
|
30
|
+
* declared" names and the register callback are all injected, so every branch
|
|
31
|
+
* is tested with no Pi session, no PATH and no real binary.
|
|
32
|
+
*/
|
|
33
|
+
/** The name the server is registered under, and what `mcp.json` would declare. */
|
|
34
|
+
export declare const ENGRAM_SERVER_NAME = "engram";
|
|
35
|
+
/** The argv the workflow's runners use (pi's own registration, measured). */
|
|
36
|
+
export declare const ENGRAM_ARGS: readonly ["mcp", "--tools=agent"];
|
|
37
|
+
/** The one documented install route (D3). */
|
|
38
|
+
export declare const ENGRAM_INSTALL_ROUTE = "pi install npm:gentle-engram@latest";
|
|
39
|
+
export type EngramStatus = "registered" | "not-consented" | "config-invalid" | "binary-missing" | "already-declared" | "unsupported-host";
|
|
40
|
+
export interface EngramServerConfig {
|
|
41
|
+
command: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface EngramProvisionInput {
|
|
45
|
+
/** The operator's consent: `config.memory.engram`. Absent means no. */
|
|
46
|
+
consented: boolean;
|
|
47
|
+
/** True when the package's config file exists but does not parse or validate.
|
|
48
|
+
* Consent is then UNKNOWN, not refused: `loadConfig` fails closed to the
|
|
49
|
+
* shipped default, so reading `consented` off that default would tell an
|
|
50
|
+
* opted-in operator to opt in — `config/load.ts` calls a present-but-invalid
|
|
51
|
+
* file a problem, never a silent default, and `dispatch.ts` refuses on the
|
|
52
|
+
* same flag. Checked before consent. */
|
|
53
|
+
configInvalid?: boolean;
|
|
54
|
+
/** Absolute path of the resolved `engram` binary, or null when not found. */
|
|
55
|
+
binaryPath: string | null;
|
|
56
|
+
/** Server names the operator already declared (a `mcp.json` name wins), read
|
|
57
|
+
* from every scope Pi itself reads — see `declaredMcpServerPaths`. */
|
|
58
|
+
declaredNames: readonly string[];
|
|
59
|
+
/** Pi's registrar, injected so this module never imports Pi. `null` when the
|
|
60
|
+
* host's ExtensionAPI predates `registerMcpServer` — the package's peer floor
|
|
61
|
+
* is a range, so an older host degrades instead of throwing. */
|
|
62
|
+
register: ((name: string, config: EngramServerConfig) => void) | null;
|
|
63
|
+
}
|
|
64
|
+
export interface EngramReport {
|
|
65
|
+
status: EngramStatus;
|
|
66
|
+
/** Present only when `status === "registered"`: exactly what was written. */
|
|
67
|
+
server?: {
|
|
68
|
+
name: string;
|
|
69
|
+
} & EngramServerConfig;
|
|
70
|
+
/** One line an operator can read back; states what was written or why not. */
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The server names an operator already declared in an `mcp.json` file. Pi gives
|
|
75
|
+
* a declared server precedence over an extension registration, so the caller
|
|
76
|
+
* passes this in and the module reports rather than shadows. Unreadable or
|
|
77
|
+
* malformed JSON yields `[]`: a file this module cannot parse is not evidence
|
|
78
|
+
* of a declaration, and refusing to register on it would be a silent gap.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readDeclaredServerNames(path: string): string[];
|
|
81
|
+
/**
|
|
82
|
+
* The union of the names declared across the `mcp.json` files Pi reads. One
|
|
83
|
+
* file's declaration is not evidence about the other's.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readDeclaredServerNamesFrom(paths: readonly string[]): string[];
|
|
86
|
+
/**
|
|
87
|
+
* The `mcp.json` files Pi reads for one session: the user-level file, and — for
|
|
88
|
+
* a trusted project only — `<cwd>/.pi/mcp.json`, whose entry replaces a
|
|
89
|
+
* same-named user entry (Pi's `mcp.md`: project configuration is read only after
|
|
90
|
+
* trust is granted, and "a project entry replaces a user-level entry with the
|
|
91
|
+
* same name"). The anti-shadow guard has to consult the same surface the host
|
|
92
|
+
* applies: reading only the user file let a project declaration slip past it, so
|
|
93
|
+
* the package registered anyway and then reported precedence over a file it had
|
|
94
|
+
* never opened.
|
|
95
|
+
*/
|
|
96
|
+
export declare function declaredMcpServerPaths({ agentDir, cwd, projectTrusted, }: {
|
|
97
|
+
agentDir: string;
|
|
98
|
+
cwd: string;
|
|
99
|
+
projectTrusted: boolean;
|
|
100
|
+
}): string[];
|
|
101
|
+
/**
|
|
102
|
+
* Resolve `engram` on `PATH` without a shell, honouring `PATHEXT` on Windows.
|
|
103
|
+
* Injected `exists`/`env` keep the lookup testable; the production caller takes
|
|
104
|
+
* the defaults.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveEngramBinary({ env, exists, platform, }?: {
|
|
107
|
+
env?: Record<string, string | undefined>;
|
|
108
|
+
exists?: (path: string) => boolean;
|
|
109
|
+
platform?: NodeJS.Platform;
|
|
110
|
+
}): string | null;
|
|
111
|
+
/**
|
|
112
|
+
* Decide and perform the registration, in the order that makes the outcome
|
|
113
|
+
* truthful: a config that cannot be read first (its consent is unknown, never
|
|
114
|
+
* refused), then consent (never provision unasked), then the binary (never
|
|
115
|
+
* register a server that cannot start), then the operator's own declaration
|
|
116
|
+
* (never shadow it). Returns the report; it never throws and never mutates
|
|
117
|
+
* anything beyond the injected `register` call.
|
|
118
|
+
*/
|
|
119
|
+
export declare function provisionEngram(input: EngramProvisionInput): EngramReport;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engram MCP provisioning (feature 66, P4 — AC3, decision D3).
|
|
3
|
+
*
|
|
4
|
+
* The memory substrate's registration is **ask-first and reversible**, which is
|
|
5
|
+
* why it goes through Pi's own consent surface instead of writing a
|
|
6
|
+
* machine-global `mcp.json` behind the operator's back:
|
|
7
|
+
*
|
|
8
|
+
* - `pi.registerMcpServer(name, config)` is session-scoped and NOT persisted
|
|
9
|
+
* (Pi's contract), so the package registers on every session start and nothing
|
|
10
|
+
* outlives the session unless the operator asks for it.
|
|
11
|
+
* - Registration happens only when the `engram` binary actually resolves — a
|
|
12
|
+
* registered-but-broken server would surface as an extension error on every
|
|
13
|
+
* session, which is a worse default than not registering.
|
|
14
|
+
* - A server the operator already declared in an `mcp.json` Pi reads — the
|
|
15
|
+
* user file or, for a trusted project, `<cwd>/.pi/mcp.json` — wins (Pi gives
|
|
16
|
+
* it precedence), so the package reports that and does not shadow it.
|
|
17
|
+
* - The operator's `memory.engram` config key is the consent switch: absent or
|
|
18
|
+
* `false` registers nothing. This module never provisions without it.
|
|
19
|
+
*
|
|
20
|
+
* Every outcome is reported verbatim — "exactly what was written" is the rule
|
|
21
|
+
* the sibling `init-workspace` consent flow already follows, and a
|
|
22
|
+
* provisioning step the operator cannot read back is one they cannot trust.
|
|
23
|
+
*
|
|
24
|
+
* The install route is `pi install npm:gentle-engram@latest`. The CLI's own
|
|
25
|
+
* setup subcommand is deliberately NOT the documented route: it pins whatever
|
|
26
|
+
* version it ran with and keeps serving it until it is run again, so an updated
|
|
27
|
+
* binary can sit unused behind a stale registration.
|
|
28
|
+
*
|
|
29
|
+
* Pi-free by construction — the binary lookup, the environment, the "already
|
|
30
|
+
* declared" names and the register callback are all injected, so every branch
|
|
31
|
+
* is tested with no Pi session, no PATH and no real binary.
|
|
32
|
+
*/
|
|
33
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
34
|
+
import { delimiter, join } from "node:path";
|
|
35
|
+
/** The directory a project keeps its own agent config in — Pi's `.pi/`. */
|
|
36
|
+
const PROJECT_DIR = ".pi";
|
|
37
|
+
/** The name the server is registered under, and what `mcp.json` would declare. */
|
|
38
|
+
export const ENGRAM_SERVER_NAME = "engram";
|
|
39
|
+
/** The argv the workflow's runners use (pi's own registration, measured). */
|
|
40
|
+
export const ENGRAM_ARGS = ["mcp", "--tools=agent"];
|
|
41
|
+
/** The one documented install route (D3). */
|
|
42
|
+
export const ENGRAM_INSTALL_ROUTE = "pi install npm:gentle-engram@latest";
|
|
43
|
+
/**
|
|
44
|
+
* The server names an operator already declared in an `mcp.json` file. Pi gives
|
|
45
|
+
* a declared server precedence over an extension registration, so the caller
|
|
46
|
+
* passes this in and the module reports rather than shadows. Unreadable or
|
|
47
|
+
* malformed JSON yields `[]`: a file this module cannot parse is not evidence
|
|
48
|
+
* of a declaration, and refusing to register on it would be a silent gap.
|
|
49
|
+
*/
|
|
50
|
+
export function readDeclaredServerNames(path) {
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = readFileSync(path, "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(text);
|
|
60
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
61
|
+
return [];
|
|
62
|
+
const servers = parsed.mcpServers;
|
|
63
|
+
if (typeof servers !== "object" || servers === null)
|
|
64
|
+
return [];
|
|
65
|
+
return Object.keys(servers);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The union of the names declared across the `mcp.json` files Pi reads. One
|
|
73
|
+
* file's declaration is not evidence about the other's.
|
|
74
|
+
*/
|
|
75
|
+
export function readDeclaredServerNamesFrom(paths) {
|
|
76
|
+
const names = new Set();
|
|
77
|
+
for (const path of paths) {
|
|
78
|
+
for (const name of readDeclaredServerNames(path))
|
|
79
|
+
names.add(name);
|
|
80
|
+
}
|
|
81
|
+
return [...names];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The `mcp.json` files Pi reads for one session: the user-level file, and — for
|
|
85
|
+
* a trusted project only — `<cwd>/.pi/mcp.json`, whose entry replaces a
|
|
86
|
+
* same-named user entry (Pi's `mcp.md`: project configuration is read only after
|
|
87
|
+
* trust is granted, and "a project entry replaces a user-level entry with the
|
|
88
|
+
* same name"). The anti-shadow guard has to consult the same surface the host
|
|
89
|
+
* applies: reading only the user file let a project declaration slip past it, so
|
|
90
|
+
* the package registered anyway and then reported precedence over a file it had
|
|
91
|
+
* never opened.
|
|
92
|
+
*/
|
|
93
|
+
export function declaredMcpServerPaths({ agentDir, cwd, projectTrusted, }) {
|
|
94
|
+
const paths = [join(agentDir, "mcp.json")];
|
|
95
|
+
if (projectTrusted)
|
|
96
|
+
paths.push(join(cwd, PROJECT_DIR, "mcp.json"));
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve `engram` on `PATH` without a shell, honouring `PATHEXT` on Windows.
|
|
101
|
+
* Injected `exists`/`env` keep the lookup testable; the production caller takes
|
|
102
|
+
* the defaults.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveEngramBinary({ env = process.env, exists = isExecutable, platform = process.platform, } = {}) {
|
|
105
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
106
|
+
if (pathValue === "")
|
|
107
|
+
return null;
|
|
108
|
+
const extensions = platform === "win32" ? splitExtensions(env.PATHEXT) : [""];
|
|
109
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
110
|
+
if (dir === "")
|
|
111
|
+
continue;
|
|
112
|
+
for (const extension of extensions) {
|
|
113
|
+
const candidate = join(dir, `${ENGRAM_SERVER_NAME}${extension}`);
|
|
114
|
+
if (exists(candidate))
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function splitExtensions(pathext) {
|
|
121
|
+
const raw = (pathext ?? ".EXE;.CMD;.BAT;.COM").split(";").filter((value) => value !== "");
|
|
122
|
+
return [...raw, ""];
|
|
123
|
+
}
|
|
124
|
+
function isExecutable(path) {
|
|
125
|
+
try {
|
|
126
|
+
accessSync(path, constants.X_OK);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decide and perform the registration, in the order that makes the outcome
|
|
135
|
+
* truthful: a config that cannot be read first (its consent is unknown, never
|
|
136
|
+
* refused), then consent (never provision unasked), then the binary (never
|
|
137
|
+
* register a server that cannot start), then the operator's own declaration
|
|
138
|
+
* (never shadow it). Returns the report; it never throws and never mutates
|
|
139
|
+
* anything beyond the injected `register` call.
|
|
140
|
+
*/
|
|
141
|
+
export function provisionEngram(input) {
|
|
142
|
+
const { consented, configInvalid, binaryPath, declaredNames, register } = input;
|
|
143
|
+
if (configInvalid === true) {
|
|
144
|
+
return {
|
|
145
|
+
status: "config-invalid",
|
|
146
|
+
message: `pi-agentic-workflow: Engram not registered — the package's config file is invalid, so ` +
|
|
147
|
+
`its consent could not be read and nothing was written (fix the reported problems and ` +
|
|
148
|
+
`the next session reads the switch again).`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!consented) {
|
|
152
|
+
return {
|
|
153
|
+
status: "not-consented",
|
|
154
|
+
message: `pi-agentic-workflow: Engram not registered — set "memory": { "engram": true } in ` +
|
|
155
|
+
`pi-agentic-workflow.json to opt in (nothing was written).`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (binaryPath === null) {
|
|
159
|
+
return {
|
|
160
|
+
status: "binary-missing",
|
|
161
|
+
message: `pi-agentic-workflow: Engram not registered — no "engram" binary on PATH, so ` +
|
|
162
|
+
`nothing was written. Install it with: ${ENGRAM_INSTALL_ROUTE}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (register === null) {
|
|
166
|
+
return {
|
|
167
|
+
status: "unsupported-host",
|
|
168
|
+
message: `pi-agentic-workflow: Engram not registered — this Pi host exposes no ` +
|
|
169
|
+
`registerMcpServer, so the server was not added (nothing was written).`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (declaredNames.includes(ENGRAM_SERVER_NAME)) {
|
|
173
|
+
return {
|
|
174
|
+
status: "already-declared",
|
|
175
|
+
message: `pi-agentic-workflow: Engram not registered — "${ENGRAM_SERVER_NAME}" is already ` +
|
|
176
|
+
`declared in an mcp.json Pi reads, which takes precedence (nothing was written).`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const config = { command: binaryPath, args: [...ENGRAM_ARGS] };
|
|
180
|
+
register(ENGRAM_SERVER_NAME, config);
|
|
181
|
+
return {
|
|
182
|
+
status: "registered",
|
|
183
|
+
server: { name: ENGRAM_SERVER_NAME, ...config },
|
|
184
|
+
message: `pi-agentic-workflow: registered MCP server "${ENGRAM_SERVER_NAME}" → ` +
|
|
185
|
+
`${binaryPath} ${ENGRAM_ARGS.join(" ")} (session-scoped; Pi does not persist it)`,
|
|
186
|
+
};
|
|
187
|
+
}
|
package/dist/extension/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readGitStatusBounded } from "./receipt-guard.js";
|
|
|
9
9
|
import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
|
|
10
10
|
import { createHintStore, createSerenaReadinessStore, stateFilePath } from "../routing/state.js";
|
|
11
11
|
import { createSerenaReadinessGate, readInitWorkspaceRevision } from "../routing/serena-readiness.js";
|
|
12
|
+
import { provisionEngram, readDeclaredServerNamesFrom, resolveEngramBinary, declaredMcpServerPaths } from "./engram.js";
|
|
12
13
|
import { runSettingsConsole } from "../settings/console.js";
|
|
13
14
|
import { readConfigFile, writeConfigFile } from "../settings/store.js";
|
|
14
15
|
import { SETTINGS_COMMAND } from "../routing/types.js";
|
|
@@ -172,6 +173,48 @@ function resolveExistingPrefix(target) {
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Pi's MCP registrar, read structurally rather than imported: the package's
|
|
178
|
+
* peer floor is a range, and a host older than `registerMcpServer` must degrade
|
|
179
|
+
* (the provisioning module reports `unsupported-host`) instead of throwing at
|
|
180
|
+
* load.
|
|
181
|
+
*/
|
|
182
|
+
function mcpRegistrar(pi) {
|
|
183
|
+
const candidate = pi.registerMcpServer;
|
|
184
|
+
if (typeof candidate !== "function")
|
|
185
|
+
return null;
|
|
186
|
+
return (name, config) => {
|
|
187
|
+
candidate.call(pi, name, config);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The server names the operator already has declared — the set the anti-shadow
|
|
192
|
+
* guard is judged against. Both surfaces are unioned: Pi's own `getMcpServers()`
|
|
193
|
+
* (read structurally for the same peer-floor reason as the registrar; it is the
|
|
194
|
+
* merged view the host applies), and the `mcp.json` files Pi reads for this
|
|
195
|
+
* session, user file and trusted project file alike. Either one answering is
|
|
196
|
+
* enough to refuse; reading only the user file was the F6 defect, which
|
|
197
|
+
* registered over a project declaration and then reported precedence over a
|
|
198
|
+
* file it had never opened.
|
|
199
|
+
*/
|
|
200
|
+
function declaredServerNames(pi, agentDir, ctx) {
|
|
201
|
+
const names = new Set(readDeclaredServerNamesFrom(declaredMcpServerPaths({ agentDir, cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() })));
|
|
202
|
+
const candidate = pi.getMcpServers;
|
|
203
|
+
if (typeof candidate === "function") {
|
|
204
|
+
try {
|
|
205
|
+
const listed = candidate.call(pi);
|
|
206
|
+
if (Array.isArray(listed)) {
|
|
207
|
+
for (const server of listed)
|
|
208
|
+
if (typeof server?.name === "string")
|
|
209
|
+
names.add(server.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// A host that cannot answer is not a declaration: the file reads stand.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [...names];
|
|
217
|
+
}
|
|
175
218
|
export default function extension(pi) {
|
|
176
219
|
const agentDir = getAgentDir();
|
|
177
220
|
const hint = createHintStore({ path: stateFilePath(agentDir) });
|
|
@@ -214,13 +257,37 @@ export default function extension(pi) {
|
|
|
214
257
|
writeFile: writeConfigFile,
|
|
215
258
|
}),
|
|
216
259
|
});
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
260
|
+
// One `session_start` listener carries both first chances, in this order.
|
|
261
|
+
//
|
|
262
|
+
// The readiness nudge (AC9) hears the session here, and the gate latches so
|
|
263
|
+
// the command path never repeats it — soft by construction, because the gate
|
|
264
|
+
// swallows a broken read and a failed latch.
|
|
265
|
+
//
|
|
266
|
+
// Memory substrate (feature 66, AC3) comes second: ask-first,
|
|
267
|
+
// registrable-not-persisted. Registration runs on the session rather than at
|
|
268
|
+
// load so an older Pi host is reported instead of throwing, and so the
|
|
269
|
+
// operator reads back exactly what was written. Nothing is provisioned
|
|
270
|
+
// without the `memory.engram` opt-in, and a config file that does not parse
|
|
271
|
+
// is never read as one: its consent is unknown, and the refusal says so
|
|
272
|
+
// instead of telling an opted-in operator to opt in.
|
|
220
273
|
pi.on("session_start", (_event, ctx) => {
|
|
221
274
|
const message = serenaReadiness.check(ctx.cwd);
|
|
222
275
|
if (message)
|
|
223
276
|
ctx.ui.notify(message, "warning");
|
|
277
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
278
|
+
const loaded = loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted });
|
|
279
|
+
const report = provisionEngram({
|
|
280
|
+
consented: loaded.config.memory?.engram === true,
|
|
281
|
+
configInvalid: !loaded.ok,
|
|
282
|
+
binaryPath: resolveEngramBinary(),
|
|
283
|
+
declaredNames: declaredServerNames(pi, agentDir, ctx),
|
|
284
|
+
register: mcpRegistrar(pi),
|
|
285
|
+
});
|
|
286
|
+
// The opt-out stays silent; every other outcome is stated once, in the
|
|
287
|
+
// module's own words (it already says what was written, or why not).
|
|
288
|
+
if (report.status !== "not-consented") {
|
|
289
|
+
ctx.ui.notify(report.message, report.status === "registered" ? "info" : "warning");
|
|
290
|
+
}
|
|
224
291
|
});
|
|
225
292
|
pi.on("model_select", (event) => router.noteModelSelect(event.model));
|
|
226
293
|
pi.on("thinking_level_select", (event) => router.noteThinkingLevelSelect(event.level));
|
package/dist/settings/console.js
CHANGED
|
@@ -650,38 +650,84 @@ function describeRouting(file) {
|
|
|
650
650
|
];
|
|
651
651
|
return routes.join(" · ");
|
|
652
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* The canonical write order for a config file. `dirty()` compares the cleaned
|
|
655
|
+
* draft against the cleaned file as JSON text, so the order is part of the
|
|
656
|
+
* contract: the same keys written in a different order would read as an edit.
|
|
657
|
+
* Keys outside this list are appended in the draft's own order, so a key the
|
|
658
|
+
* console never prompts for still survives the save (F4).
|
|
659
|
+
*/
|
|
660
|
+
const KEY_ORDER = [
|
|
661
|
+
"default",
|
|
662
|
+
"commands",
|
|
663
|
+
"onUnavailableRoute",
|
|
664
|
+
"onSettle",
|
|
665
|
+
"pathProtection",
|
|
666
|
+
"advance",
|
|
667
|
+
"recommendedModels",
|
|
668
|
+
"memory",
|
|
669
|
+
"profiles",
|
|
670
|
+
"profileOrder",
|
|
671
|
+
"profileFallback",
|
|
672
|
+
];
|
|
653
673
|
/**
|
|
654
674
|
* Keep everything the file held or the operator set; drop only what says nothing
|
|
655
675
|
* (an empty route). Eliding by VALUE — treating an explicit `inherit` or `stop` as
|
|
656
676
|
* absent because a shipped default happens to agree — silently discards the only
|
|
657
677
|
* two moves that matter at project scope: switching a command off under a global
|
|
658
678
|
* route, and re-arming fail-closed over a global `inherit` (F4).
|
|
679
|
+
*
|
|
680
|
+
* The copy is driven by the draft's OWN keys, not by a list of the keys this
|
|
681
|
+
* console edits. Enumerating them silently deleted every valid key the console
|
|
682
|
+
* never prompts for — `memory` (the Engram consent switch, feature 66 AC3),
|
|
683
|
+
* `pathProtection` and `advance` — so an opt-in written by hand vanished on the
|
|
684
|
+
* next save with no message and no error (F4/VF-4). A key the parser accepted is
|
|
685
|
+
* never a key this function may drop.
|
|
659
686
|
*/
|
|
660
687
|
function clean(draft) {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
688
|
+
const kept = {};
|
|
689
|
+
for (const [key, value] of Object.entries(draft)) {
|
|
690
|
+
if (value === undefined)
|
|
691
|
+
continue;
|
|
692
|
+
if (key === "commands") {
|
|
693
|
+
const commands = {};
|
|
694
|
+
for (const [name, route] of Object.entries((value ?? {}))) {
|
|
695
|
+
if (route && Object.keys(route).length > 0)
|
|
696
|
+
commands[name] = { ...route };
|
|
697
|
+
}
|
|
698
|
+
if (Object.keys(commands).length > 0)
|
|
699
|
+
kept.commands = commands;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (key === "profiles") {
|
|
703
|
+
const profiles = {};
|
|
704
|
+
for (const [name, profile] of Object.entries((value ?? {}))) {
|
|
705
|
+
if (profile && Object.keys(profile).length > 0)
|
|
706
|
+
profiles[name] = structuredClone(profile);
|
|
707
|
+
}
|
|
708
|
+
if (Object.keys(profiles).length > 0)
|
|
709
|
+
kept.profiles = profiles;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(value)) {
|
|
713
|
+
// `profileOrder` is the only list at this level: an empty order says
|
|
714
|
+
// nothing, so it is elided exactly as the empty route was.
|
|
715
|
+
if (value.length > 0)
|
|
716
|
+
kept[key] = [...value];
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (typeof value === "object" && value !== null && Object.keys(value).length === 0)
|
|
720
|
+
continue;
|
|
721
|
+
kept[key] = typeof value === "object" && value !== null ? structuredClone(value) : value;
|
|
679
722
|
}
|
|
680
|
-
|
|
681
|
-
|
|
723
|
+
const file = {};
|
|
724
|
+
for (const key of KEY_ORDER) {
|
|
725
|
+
if (key in kept)
|
|
726
|
+
file[key] = kept[key];
|
|
682
727
|
}
|
|
683
|
-
|
|
684
|
-
|
|
728
|
+
for (const key of Object.keys(kept)) {
|
|
729
|
+
if (!(key in file))
|
|
730
|
+
file[key] = kept[key];
|
|
685
731
|
}
|
|
686
732
|
return file;
|
|
687
733
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: execute-phase
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 6.1
|
|
4
|
+
version: 6.2.1
|
|
5
5
|
argument-hint: <NN-slug> [P<k>|step] [--max-attempts N] [--force]
|
|
6
6
|
allowed-tools: [Bash, Read, Edit, Write, MultiEdit]
|
|
7
7
|
author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
|
|
@@ -44,6 +44,8 @@ done:
|
|
|
44
44
|
✓ Evidence rows updated (command → exit/digest → output → verified-by)
|
|
45
45
|
✓ Progress log updated with a dated `YYYY-MM-DD HH:MM` entry that names the
|
|
46
46
|
phase id (what, commit, next)
|
|
47
|
+
✓ Memory boundary ran — the phase's outcome saved under `unit/<NN-slug>/doc`,
|
|
48
|
+
or the mirror marked pending when the layer is degraded
|
|
47
49
|
✓ Diff guard passed (on implement steps) — see `diff-guard` in PREFLIGHT.md
|
|
48
50
|
✓ → Next: block printed as the absolute last output
|
|
49
51
|
```
|
|
@@ -136,7 +138,14 @@ names before implementation), then green. UI/adapter glue may test after.
|
|
|
136
138
|
output (≤2 lines) | verified-by |`. Append to the Evidence section.
|
|
137
139
|
3. **Progress log**: append a dated entry —
|
|
138
140
|
`YYYY-MM-DD HH:MM — <what was done> → <commit sha> — next: <what is next>`.
|
|
139
|
-
4. **
|
|
141
|
+
4. **Memory boundary** — this step is a phase transition. Follow
|
|
142
|
+
[the memory protocol](../unit-lane/references/MEMORY.md): before the first
|
|
143
|
+
memory read run `agentic-workflow memory probe` once and record its verdict
|
|
144
|
+
in Evidence; after the step, save the phase id, what it changed, its Evidence
|
|
145
|
+
outcome and any discovery under `topic_key: "unit/<NN-slug>/doc"`, an explicit
|
|
146
|
+
`project` and explicit session attribution (§5). A `degraded` verdict is
|
|
147
|
+
recorded and the mirror marked pending — it never blocks the step.
|
|
148
|
+
5. **Commit**: `git add <changed files>` then `git commit -m "<type>(<scope>):
|
|
140
149
|
<step-summary>"` — run it, paste the SHA.
|
|
141
150
|
|
|
142
151
|
### 6. Red gate (repair loop)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: init-workspace
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
argument-hint: <target-dir>
|
|
6
6
|
author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -78,7 +78,7 @@ Inspect the target dir (`[target-dir]`, default cwd) before touching anything:
|
|
|
78
78
|
|
|
79
79
|
## Progressive loading — bootstrap or upgrade, never both
|
|
80
80
|
|
|
81
|
-
The reference allowlist is exactly the
|
|
81
|
+
The reference allowlist is exactly the six linked paths below. Never invent or
|
|
82
82
|
read another `references/` path. Step 0 selects exactly one route:
|
|
83
83
|
|
|
84
84
|
**Hard rule for an existing Claude Code, Cursor, Copilot, or OpenCode
|
|
@@ -95,6 +95,10 @@ or portability unless the runtime platform has no named adapter at all.
|
|
|
95
95
|
only. Its additive-only rule is absolute; do not replay bootstrap.
|
|
96
96
|
The upgrade route loads [SERENA language servers](references/SERENA.md)
|
|
97
97
|
for the additive provisioning step.
|
|
98
|
+
- Provisioning Engram on a host with no pi package: read
|
|
99
|
+
[Engram provisioning](references/ENGRAM.md) — detect, ask, register or
|
|
100
|
+
instruct, report. Loaded by the bootstrap write route only (the pi package
|
|
101
|
+
provisions itself, feature 66).
|
|
98
102
|
- No named adapter exists for the detected runtime platform: add
|
|
99
103
|
[portability](references/PORTABILITY.md) only for that fallback. Claude Code,
|
|
100
104
|
Cursor, Copilot, and OpenCode are named supported adapters; a missing adapter
|
|
@@ -133,6 +137,13 @@ loads exactly upgrade and skips bootstrap plus portability.
|
|
|
133
137
|
auto-detected), a listed server that fails to start must be removed (one
|
|
134
138
|
failure disables every server in the project), and LSP evidence stays
|
|
135
139
|
TypeScript/JavaScript-only. Document verification is a script's job.
|
|
140
|
+
- **Engram provisioning is ask-first, never assumed (feature 66).** Detect the
|
|
141
|
+
`engram` binary, ask before writing any host MCP config, write the absolute
|
|
142
|
+
path with the exact `mcp --tools=agent` argv, and report exactly what was
|
|
143
|
+
written. An existing `engram` declaration is never overwritten, and an absent
|
|
144
|
+
binary is a recorded residual — the documented install route is
|
|
145
|
+
`pi install npm:gentle-engram@latest`, never the CLI's own setup subcommand,
|
|
146
|
+
which keeps serving whatever version ran it until it is re-run.
|
|
136
147
|
- **Retrieval index scaffold and git hooks are consent-gated (AC22).** The
|
|
137
148
|
index surface — the committed `.agentic-workflow/index.json` config plus the
|
|
138
149
|
three freshness hooks (`post-merge`, `post-checkout`, `post-rewrite`, all
|
|
@@ -34,7 +34,13 @@
|
|
|
34
34
|
health check and render the fixed provisioning report. When no installer is
|
|
35
35
|
available, offer only what the machine can perform and record the residual
|
|
36
36
|
with its reason — the run completes either way.
|
|
37
|
-
9. **
|
|
37
|
+
9. **Offer Engram provisioning.** Load [ENGRAM](ENGRAM.md), detect the binary
|
|
38
|
+
and ask consent: register the `engram` MCP server (absolute path, exact
|
|
39
|
+
`mcp --tools=agent` argv) when the host has one, leave an existing
|
|
40
|
+
declaration untouched, and report exactly what was written. An absent binary
|
|
41
|
+
becomes a residual with the documented install route — never a failed run.
|
|
42
|
+
Skip this step when the pi package provisions the host itself.
|
|
43
|
+
10. **Install accepted agent safety hooks.** Keep the canonical policy and
|
|
38
44
|
fullauto wrapper at `.agentic-workflow/hooks/`. Seed the shipped
|
|
39
45
|
path-protection policy at `.agentic-workflow/path-policy.json` and its doc
|
|
40
46
|
page at `.agentic-workflow/path-protection.md`, beside the hooks — the
|
|
@@ -47,10 +53,10 @@
|
|
|
47
53
|
an unknown payload, or an existing customized hook file becomes a residual,
|
|
48
54
|
never a silent overwrite. Direct merges remain blocked; do not create a
|
|
49
55
|
`.automerge` exception.
|
|
50
|
-
|
|
56
|
+
11. **Offer the workflow skills.** Propose installing them:
|
|
51
57
|
`npx skills add gtrabanco/agentic-workflow` (note the SSH/local-path variant if
|
|
52
58
|
the source is private). Don't install without a yes.
|
|
53
|
-
|
|
59
|
+
12. **State that reviews are self-contained; offer optional extras.** The
|
|
54
60
|
workflow ships its **own internal review pack** (`review-code`,
|
|
55
61
|
`review-security`, `review-verify`, `review-debt`, `review-design`,
|
|
56
62
|
`review-a11y`, `review-brand`, `review-perf`, `review-seo`) — it installs
|
|
@@ -60,7 +66,7 @@
|
|
|
60
66
|
under a short "Optional review extras" note so `review-change` and
|
|
61
67
|
`product-audit` run them **in addition** — never as a dependency. Don't
|
|
62
68
|
install anything without a yes.
|
|
63
|
-
|
|
69
|
+
13. **Seed the urgency labels (feature 15, injection-safe urgency).** Create the
|
|
64
70
|
two capability-gated GitHub labels `triage-issue` owns and applies
|
|
65
71
|
(`skills/triage-issue/SKILL.md` is the sole owner of the name/color
|
|
66
72
|
vocabulary — this step only seeds it, never redefines it):
|
|
@@ -73,7 +79,7 @@
|
|
|
73
79
|
forge is unavailable or the user declines forge setup, skip this step and
|
|
74
80
|
list the two labels as a residual for the user to create manually later
|
|
75
81
|
(never fail the whole scaffold on it).
|
|
76
|
-
|
|
82
|
+
14. **Report.** List what was created, which placeholders still need human input,
|
|
77
83
|
the companion skills recorded/installed, the urgency labels seeded (or
|
|
78
84
|
skipped, with reason), and the next step:
|
|
79
85
|
`discover-repository-state` → `/unit-lane` (or `workflow-status`) → catalog
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
## Why this step exists
|
|
2
|
+
|
|
3
|
+
The workflow's units save what they learned into Engram, the cross-session
|
|
4
|
+
memory layer its memory protocol documents. On a host whose agent has no `pi`
|
|
5
|
+
package to provision it (the pi package registers the server itself, feature
|
|
6
|
+
66 P4), the target project has no memory layer at all unless this step offers
|
|
7
|
+
one — so Engram provisioning is a **parity obligation**, not an optional extra:
|
|
8
|
+
feature 66's approval is that the pi path registers it and every other host is
|
|
9
|
+
offered the same thing ask-first.
|
|
10
|
+
|
|
11
|
+
## What this file owns
|
|
12
|
+
|
|
13
|
+
The **non-pi** half of Engram provisioning. It owns exactly four moves —
|
|
14
|
+
detect, ask, provision-or-instruct, report — and nothing else. The workflow-side
|
|
15
|
+
protocol (when units save, the recall order, the degraded path) is **not** here;
|
|
16
|
+
it lives in `unit-lane`'s memory reference, which the skills that run units
|
|
17
|
+
already load.
|
|
18
|
+
|
|
19
|
+
## 1. Detect
|
|
20
|
+
|
|
21
|
+
Look for the `engram` binary on `PATH` (`command -v engram`) and read its
|
|
22
|
+
version (`engram --version`). Two outcomes matter:
|
|
23
|
+
|
|
24
|
+
- **Present** — record the resolved path and the version fingerprint.
|
|
25
|
+
- **Absent** — record it as a residual, and go to step 2 with the install
|
|
26
|
+
guidance below rather than silently skipping the step.
|
|
27
|
+
|
|
28
|
+
Never write an MCP registration file for a binary that does not resolve: a
|
|
29
|
+
registered-but-unstartable server fails every session after it, which is a worse
|
|
30
|
+
default than an honest gap.
|
|
31
|
+
|
|
32
|
+
## 2. Ask, then do exactly what was agreed
|
|
33
|
+
|
|
34
|
+
This is a **consent-gated install** like every other in this skill. Ask one
|
|
35
|
+
question — "register Engram as the project's memory layer?" — and act on the
|
|
36
|
+
answer:
|
|
37
|
+
|
|
38
|
+
| Answer | What you do |
|
|
39
|
+
|---|---|
|
|
40
|
+
| yes | write the MCP registration named in step 3 and report it verbatim |
|
|
41
|
+
| no | write nothing, record it as a residual, finish the run normally |
|
|
42
|
+
| not installed | offer the install route below, and provision **only** if the user then says yes |
|
|
43
|
+
|
|
44
|
+
**The install route is `pi install npm:gentle-engram@latest`.** The CLI's own
|
|
45
|
+
setup subcommand is **not** the documented route and must never be printed as an
|
|
46
|
+
instruction: it pins whatever version ran it and keeps serving that version
|
|
47
|
+
until it is re-run, so an updated binary can sit unused behind a stale
|
|
48
|
+
registration (decision D3, 2026-10-21).
|
|
49
|
+
|
|
50
|
+
## 3. What "register" means on a non-pi host
|
|
51
|
+
|
|
52
|
+
There is no `pi.registerMcpServer` here, so the registration is a **declared
|
|
53
|
+
file** the host agent reads. Write the server into that host's own MCP config
|
|
54
|
+
(the agent's documented MCP surface), never into a file the host does not read:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"engram": { "command": "<absolute path to engram>", "args": ["mcp", "--tools=agent"] }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Rules:**
|
|
65
|
+
|
|
66
|
+
- **Absolute path, never a bare name** — the host's `PATH` at load time is not
|
|
67
|
+
the shell's.
|
|
68
|
+
- **Args are exactly** `mcp` `--tools=agent`. A different argv changes which
|
|
69
|
+
tools the server exposes.
|
|
70
|
+
- **Never overwrite a server the user already declared** under the name
|
|
71
|
+
`engram` — a declared server wins over anything this step would add. Report
|
|
72
|
+
that you found it and left it alone.
|
|
73
|
+
- **Additive and idempotent** — a second run with the same content writes
|
|
74
|
+
nothing and says so.
|
|
75
|
+
|
|
76
|
+
## 4. Report exactly what was written
|
|
77
|
+
|
|
78
|
+
Every outcome ends in a report the user can read back:
|
|
79
|
+
|
|
80
|
+
- **Registered** — the file path, the server name, and the exact `command` +
|
|
81
|
+
`args` written.
|
|
82
|
+
- **Declined** — that nothing was written, and that the run continued.
|
|
83
|
+
- **Binary missing** — that nothing was written, the install route verbatim, and
|
|
84
|
+
that it is recorded as a residual.
|
|
85
|
+
- **Already declared** — the file that already carries it, and that it was left
|
|
86
|
+
untouched.
|
|
87
|
+
|
|
88
|
+
"Report exactly what was written" is the same contract the Serena step follows;
|
|
89
|
+
a provisioning step the user cannot read back is one they cannot trust.
|
|
90
|
+
|
|
91
|
+
## Checklist (pass only if)
|
|
92
|
+
|
|
93
|
+
- [ ] `engram` presence **and** version were detected (or absence recorded)
|
|
94
|
+
- [ ] Consent was asked before any file was written — never assumed
|
|
95
|
+
- [ ] The written server uses the absolute binary path and the exact argv
|
|
96
|
+
- [ ] An existing `engram` declaration was left untouched and reported
|
|
97
|
+
- [ ] A second run is a no-op
|
|
98
|
+
- [ ] The closing report states what was written — or that nothing was
|
|
99
|
+
|
|
100
|
+
## Forbidden
|
|
101
|
+
|
|
102
|
+
- Never print the CLI's own setup subcommand as the install route
|
|
103
|
+
- Never write an MCP config for an unresolved binary
|
|
104
|
+
- Never overwrite an operator's existing `engram` declaration
|
|
105
|
+
- Never let a memory gap fail the run — record it as a residual and continue
|
|
106
|
+
- Never restate the unit-boundary save/recall protocol here; that is the lane's
|
|
107
|
+
memory reference
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: log-session
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
argument-hint: "[note to prepend to the entry]"
|
|
6
6
|
author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -124,6 +124,22 @@ the HEAD sha and start time at session open.
|
|
|
124
124
|
open a PR just for a log entry, and never `git add -A` here — a concurrent
|
|
125
125
|
conversation's work must not be swept into a log commit.
|
|
126
126
|
|
|
127
|
+
## The canonical split — this skill's half (feature 66, D2)
|
|
128
|
+
|
|
129
|
+
Two writers exist on purpose; neither mirrors the other:
|
|
130
|
+
|
|
131
|
+
- **`docs/LOGS.md` (this skill)** is the **human/team journal** — what a session
|
|
132
|
+
set out to do, what it decided, where to resume. It travels with git, so a
|
|
133
|
+
teammate or a fresh clone reads it with no local state.
|
|
134
|
+
- **Engram (`mem_session_summary`, and the lane's `unit/<NN-slug>/doc` topic)**
|
|
135
|
+
is **local decision and gotcha recall keyed by unit id**, written by
|
|
136
|
+
`unit-lane` and `execute-phase` at unit boundaries. It stays on the machine.
|
|
137
|
+
|
|
138
|
+
Engram never mirrors this log, and this log never mirrors Engram: team memory
|
|
139
|
+
travels with git, agent recall stays local. Write the journal half here; the
|
|
140
|
+
protocol half (boundaries, recall order, degradation) lives in the lane's
|
|
141
|
+
memory reference, `../unit-lane/references/MEMORY.md`.
|
|
142
|
+
|
|
127
143
|
## Guardrails
|
|
128
144
|
|
|
129
145
|
- **Never use an expensive model for this.** It's summarization; `qwen3.6`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unit-lane
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.1
|
|
5
5
|
argument-hint: <NN-slug | "<idea>"> [--retriage] | --fix <issue-number> | --from-issue <issue-number>
|
|
6
6
|
description: >
|
|
7
7
|
One-shot lane conductor for a delivery unit: triage, implement, evidence, and
|
|
@@ -39,6 +39,8 @@ done:
|
|
|
39
39
|
on BREACH: re-triage or record exception, never shrink the diff
|
|
40
40
|
✓ Unit doc's Evidence rows updated (command → exit/digest → output → verified-by)
|
|
41
41
|
and Progress log updated (dated entry) before every commit
|
|
42
|
+
✓ Memory boundaries ran — probe verdict once, saves at unit start / each phase
|
|
43
|
+
transition / close under `unit/<NN-slug>/doc`, degradation recorded as pending
|
|
42
44
|
✓ → Next: block printed as the absolute last output
|
|
43
45
|
```
|
|
44
46
|
|
|
@@ -167,6 +169,20 @@ Before every commit:
|
|
|
167
169
|
- **Progress log**: `YYYY-MM-DD HH:MM — <what was done> → <commit sha> — next:
|
|
168
170
|
<what is next>`
|
|
169
171
|
|
|
172
|
+
### 5b. Memory boundaries
|
|
173
|
+
|
|
174
|
+
Engram is the required memory substrate; a down layer never blocks a unit —
|
|
175
|
+
follow [the memory protocol](references/MEMORY.md).
|
|
176
|
+
|
|
177
|
+
- **Probe once**, before the first memory read: `agentic-workflow memory probe`
|
|
178
|
+
→ paste the verdict into Evidence; `degraded` is recorded and said once.
|
|
179
|
+
- **Save at unit start, each phase transition, and close** under
|
|
180
|
+
`topic_key: "unit/<NN-slug>/doc"`, with explicit `project` and explicit
|
|
181
|
+
session attribution (§5 names what carries it on each write surface).
|
|
182
|
+
- **Recall** `mem_context` → scoped search → one explicit `all_projects: true`
|
|
183
|
+
retry → `mem_get_observation` → the unit doc wins. A miss is keyword-bound,
|
|
184
|
+
never proof nothing was saved; an unavailable mirror is marked pending.
|
|
185
|
+
|
|
170
186
|
### 6. Close the unit — the close-out ladder
|
|
171
187
|
|
|
172
188
|
When all triaged steps are done, in order:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Memory protocol — save and recall at unit boundaries
|
|
2
|
+
|
|
3
|
+
Engram is this workflow's required cross-session memory substrate (owner
|
|
4
|
+
decision 2026-09-24, feature 66). **Required substrate is not a hard
|
|
5
|
+
dependency:** the committed docs stay authoritative, and a memory layer that is
|
|
6
|
+
down is reported once and recorded — it never blocks a unit.
|
|
7
|
+
|
|
8
|
+
This file is the protocol's single home. `execute-phase` and `log-session` carry
|
|
9
|
+
a thin instruction plus a pointer here; they never restate it.
|
|
10
|
+
|
|
11
|
+
## 1. Probe before the first memory read
|
|
12
|
+
|
|
13
|
+
Run the bounded health probe once per session, before the first read:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
agentic-workflow memory probe # human block
|
|
17
|
+
agentic-workflow memory probe --json # memory-probe@1 verdict
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It is bounded (4 s / 10 s per check, 30 s ceiling), writes nothing, issues no
|
|
21
|
+
MCP tool call, and **always exits 0**. Paste its verdict into the unit doc's
|
|
22
|
+
Evidence section — one row, `verified-by` naming the session:
|
|
23
|
+
|
|
24
|
+
- `ok` → memory is trustworthy for this session.
|
|
25
|
+
- `degraded` → say **once** which checks failed, record the verdict, and keep
|
|
26
|
+
working. Never re-probe in a loop, never ask the user to fix it first.
|
|
27
|
+
|
|
28
|
+
A `degraded` verdict is an answer, not a blocker: the degraded path in §7 is the
|
|
29
|
+
documented response.
|
|
30
|
+
|
|
31
|
+
## 2. Boundary saves — what, when, and under which key
|
|
32
|
+
|
|
33
|
+
Save at **three boundaries**, always under a stable per-unit topic key:
|
|
34
|
+
|
|
35
|
+
| Boundary | topic_key | Payload |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| unit start (after triage) | `unit/<NN-slug>/doc` | goal, the numbered acceptance criteria verbatim, the triaged step list |
|
|
38
|
+
| each phase transition | `unit/<NN-slug>/doc` | the phase id, what it changed, its Evidence outcome, new discoveries |
|
|
39
|
+
| unit close | `unit/<NN-slug>/doc` | final Evidence outcomes, decisions taken, the gotchas worth recalling |
|
|
40
|
+
|
|
41
|
+
One payload, four parts — the same four a later session needs to resume without
|
|
42
|
+
re-reading the docs:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
memory-payload@1
|
|
46
|
+
goal: <one line — the unit's objective>
|
|
47
|
+
ac: 1. <numbered AC verbatim, one per line>
|
|
48
|
+
evidence: <phase> <command> → <exit/digest> → <outcome>
|
|
49
|
+
learned: <gotcha, edge case, or decision a fresh session would get wrong>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Call `mem_save` with `topic_key: "unit/<NN-slug>/doc"` and an explicit
|
|
53
|
+
`project`; on a write surface that exposes `session_id`, pass it too (§5). Later
|
|
54
|
+
boundaries reuse the same `topic_key` so the record **upserts** instead of
|
|
55
|
+
fragmenting into one row per phase.
|
|
56
|
+
|
|
57
|
+
**What is saved is a summary for recall, never the artifact.** The unit doc, its
|
|
58
|
+
Evidence rows and `docs/LOGS.md` stay authoritative; a memory record that
|
|
59
|
+
disagrees with them is wrong by definition.
|
|
60
|
+
|
|
61
|
+
## 3. Recall — the fixed order
|
|
62
|
+
|
|
63
|
+
Resume in this order, and stop as soon as the unit is reconstructable:
|
|
64
|
+
|
|
65
|
+
1. **`mem_context`** — scoped explicitly (`project: "<name>"`, and `scope` when
|
|
66
|
+
you mean one).
|
|
67
|
+
2. **`mem_search`** — a scoped search with an **explicit `project`**. Never rely
|
|
68
|
+
on an omitted selector for a global read; a default is not a decision.
|
|
69
|
+
3. **One retry** — if the project-scoped search misses, retry **once** with
|
|
70
|
+
**`all_projects: true`** passed explicitly. A second miss is a miss.
|
|
71
|
+
4. **`mem_get_observation`** — fetch the full untruncated body of the hit that
|
|
72
|
+
matters.
|
|
73
|
+
5. **Read the unit doc** and reconcile: the file wins on every conflict, and the
|
|
74
|
+
difference is worth recording as a discovery (§2 `learned:`).
|
|
75
|
+
|
|
76
|
+
Only when `mem_context` and both searches miss does a fresh session fall back to
|
|
77
|
+
reading the docs cold — and that fallback is recorded, because it means the
|
|
78
|
+
recall path failed, not that nothing was ever saved.
|
|
79
|
+
|
|
80
|
+
When the cwd is ambiguous (several repos under it), call **`mem_current_project`**
|
|
81
|
+
and navigate by its `available_projects`: pick the matching project explicitly
|
|
82
|
+
rather than letting an ambiguous cwd decide. An ambiguous answer is never
|
|
83
|
+
resolved by guessing.
|
|
84
|
+
|
|
85
|
+
## 4. Keyword-bound recall — the disclosed ceiling
|
|
86
|
+
|
|
87
|
+
Engram's recall is **keyword-bound** (FTS5 over stored text, no vectors). Two
|
|
88
|
+
consequences are obligations, not trivia:
|
|
89
|
+
|
|
90
|
+
- **A miss is not evidence that knowledge was never saved.** It means the query
|
|
91
|
+
did not hit the stored terms. Rephrase with the unit's own vocabulary (slug,
|
|
92
|
+
AC wording, file paths) before concluding anything is absent.
|
|
93
|
+
- **What you save must be searchable later.** Save the words a future session
|
|
94
|
+
will search for — the unit slug, the phase id, the failing command, the file
|
|
95
|
+
path — not a paraphrase.
|
|
96
|
+
|
|
97
|
+
Hybrid FTS5+vector recall is Engram's own direction (#192), never built here.
|
|
98
|
+
|
|
99
|
+
## 5. Session attribution — explicit on every write, on the surface that carries it
|
|
100
|
+
|
|
101
|
+
Every write must be **attributed to one known session**, never to whichever
|
|
102
|
+
session a machine happens to resolve. On a box with several active runtime
|
|
103
|
+
sessions an unresolved attribution fails closed and the write is lost — the one
|
|
104
|
+
outcome this protocol exists to prevent (the health probe's
|
|
105
|
+
`session-ambiguity` check reads exactly this signal, §1).
|
|
106
|
+
|
|
107
|
+
Which parameter carries that attribution depends on the surface in front of you:
|
|
108
|
+
|
|
109
|
+
| Write surface | How the session is carried | What you do |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Engram's own MCP server (`engram mcp --tools=agent`) | its `mem_save`, `mem_save_prompt`, `mem_session_summary` and `mem_capture_passive` take a `session_id` argument | open the session (`mem_session_start`) and pass that id on **every** write |
|
|
112
|
+
| a host whose memory tools expose no `session_id` (Pi's Engram provider binds the runtime session itself) | the provider stamps the session from the live runtime session and **refuses** when that identity is ambiguous or disagrees with the project | never write before the session is open, always pass an explicit `project` (and `cwd` when the directory could match more than one project), and treat an attribution refusal as a failed write — record it (§7), do not retry blind |
|
|
113
|
+
|
|
114
|
+
On both surfaces the obligation is the same and the check is the same: **a write
|
|
115
|
+
that cannot say which session it belongs to is a write that did not happen**.
|
|
116
|
+
|
|
117
|
+
- `mem_session_start` opens the id (or the runtime session the provider binds to)
|
|
118
|
+
at unit start; the boundaries in §2 reuse it, and nothing is written before it.
|
|
119
|
+
- **`mem_session_end` runs only on confirmed-stale ids** — ids you opened yourself
|
|
120
|
+
and know are finished. Never end an id a live session may still hold.
|
|
121
|
+
- When `mem_session_summary` cannot resolve the project, the explicit
|
|
122
|
+
**project fallback** is the documented escape hatch: pass `project` by name
|
|
123
|
+
and the summary lands instead of being refused. Record the fallback when you
|
|
124
|
+
use it.
|
|
125
|
+
- Passing a `session_id` to a tool that does not declare it is not "explicit
|
|
126
|
+
attribution", it is an invalid call. Read the tool's own parameter list when
|
|
127
|
+
the host offers more than one memory surface, and record which surface you used.
|
|
128
|
+
|
|
129
|
+
## 6. The canonical split (D2)
|
|
130
|
+
|
|
131
|
+
Both writers exist on purpose; neither mirrors the other.
|
|
132
|
+
|
|
133
|
+
| Writer | Owns | Travels |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `docs/LOGS.md` (`log-session`) | the human/team journal: what happened, for people | with git |
|
|
136
|
+
| `mem_session_summary` / `unit/<NN-slug>/doc` | local decision and gotcha recall keyed by unit id | local only |
|
|
137
|
+
|
|
138
|
+
Team memory travels with git; agent recall stays local. A double-write on
|
|
139
|
+
purpose, recorded here so it is never mistaken for an accident.
|
|
140
|
+
|
|
141
|
+
## 7. Degradation — recorded, never silent
|
|
142
|
+
|
|
143
|
+
When the probe says `degraded` or a write fails:
|
|
144
|
+
|
|
145
|
+
- **Local progress is preserved.** The unit doc, Evidence rows and commit are
|
|
146
|
+
the record; they never depend on memory.
|
|
147
|
+
- **The mirror is marked pending explicitly** — one line in the Progress entry
|
|
148
|
+
or Evidence row saying the memory write is pending, so a later session knows
|
|
149
|
+
the gap exists and is not a "nothing was saved" conclusion.
|
|
150
|
+
- **Unrelated safe work continues.** Never stop a unit for a mirror failure.
|
|
151
|
+
- **Sync resumes when the layer answers** — retry the pending write at the next
|
|
152
|
+
boundary, not in a loop.
|
|
153
|
+
- **Read both sides back.** After a write, read the file and the mirror; when
|
|
154
|
+
the versions are irreconcilable, keep both and surface the difference rather
|
|
155
|
+
than silently picking one.
|
|
156
|
+
|
|
157
|
+
## Checklist (pass only if)
|
|
158
|
+
|
|
159
|
+
- [ ] The probe ran once and its verdict is in Evidence (§1)
|
|
160
|
+
- [ ] A save happened at unit start, at each phase transition, and at close (§2)
|
|
161
|
+
- [ ] Every save was explicitly attributed: a `session_id` on a surface that
|
|
162
|
+
accepts one, otherwise an open session plus an explicit `project` — and it
|
|
163
|
+
named the unit's `topic_key` (§2, §5)
|
|
164
|
+
- [ ] Recall followed the fixed order, with an explicit `project` and at most
|
|
165
|
+
one explicit `all_projects: true` retry (§3)
|
|
166
|
+
- [ ] Any miss was treated as keyword-bound, not as absence (§4)
|
|
167
|
+
- [ ] Any degradation was recorded as pending — never silent (§7)
|
|
168
|
+
|
|
169
|
+
## Forbidden
|
|
170
|
+
|
|
171
|
+
- Never let a memory verdict block a unit, or re-probe in a loop
|
|
172
|
+
- Never rely on an omitted selector for a global read
|
|
173
|
+
- Never write with unresolved session attribution — pass the `session_id` where
|
|
174
|
+
the surface takes one, and where it does not, never write before the session is
|
|
175
|
+
open (§5)
|
|
176
|
+
- Never call `mem_session_end` on an id you did not open or cannot confirm stale
|
|
177
|
+
- Never mirror `docs/LOGS.md` verbatim — the split is deliberate (§6)
|
|
178
|
+
- Never treat a recall miss as proof nothing was saved
|
|
179
|
+
- Never let a memory record override the committed artifact
|