gentle-pi 0.4.5 → 0.6.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/assets/agents/review-readability.md +24 -0
- package/assets/agents/review-reliability.md +25 -0
- package/assets/agents/review-resilience.md +24 -0
- package/assets/agents/review-risk.md +24 -0
- package/assets/chains/4r-review.chain.md +39 -0
- package/assets/orchestrator.md +26 -1
- package/extensions/gentle-ai.ts +363 -25
- package/lib/review-triggers.ts +414 -0
- package/package.json +1 -1
- package/tests/artifact-language.test.ts +27 -0
- package/tests/autonomous-guard.test.ts +537 -0
- package/tests/persona-neutral-voseo.test.ts +119 -0
- package/tests/review-gate.test.ts +102 -0
- package/tests/review-triggers.test.ts +382 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-triggers.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure trigger logic for the 4R review gate system. No I/O, fully unit-testable.
|
|
5
|
+
* Ported 1:1 from gentle-ai/internal/catalog/triggers.go and
|
|
6
|
+
* gentle-ai/internal/model/trigger.go.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type TriggerEvent =
|
|
14
|
+
| "pre-commit"
|
|
15
|
+
| "pre-push"
|
|
16
|
+
| "pre-pr"
|
|
17
|
+
| "post-sdd-phase"
|
|
18
|
+
| "on-ci"
|
|
19
|
+
| "on-schedule";
|
|
20
|
+
|
|
21
|
+
export type TriggerMode = "advisory" | "strong";
|
|
22
|
+
|
|
23
|
+
export interface TriggerWhen {
|
|
24
|
+
always?: boolean;
|
|
25
|
+
pathGlobs?: string[];
|
|
26
|
+
minDiffLines?: number;
|
|
27
|
+
phases?: string[];
|
|
28
|
+
combine?: "" | "or" | "and";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TriggerBinding {
|
|
32
|
+
on: TriggerEvent;
|
|
33
|
+
when: TriggerWhen;
|
|
34
|
+
run: string[];
|
|
35
|
+
mode: TriggerMode;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TriggerRuleSet {
|
|
40
|
+
bindings: TriggerBinding[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ChangedDiff {
|
|
44
|
+
changedPaths: string[];
|
|
45
|
+
changedLines: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Constants
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Minimum number of changed lines in a diff that triggers the full 4R review
|
|
54
|
+
* fan-out on pre-pr events. Mirrors defaultLargeChangedLineThreshold in triggers.go.
|
|
55
|
+
*/
|
|
56
|
+
export const LARGE_CHANGED_LINE_THRESHOLD = 400;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Closed set of recognized agent identifiers.
|
|
60
|
+
* Mirrors knownAgentList in triggers.go.
|
|
61
|
+
*/
|
|
62
|
+
export const KNOWN_AGENTS: readonly string[] = [
|
|
63
|
+
// 4R review lenses
|
|
64
|
+
"review-risk",
|
|
65
|
+
"review-readability",
|
|
66
|
+
"review-reliability",
|
|
67
|
+
"review-resilience",
|
|
68
|
+
// Adversarial verification
|
|
69
|
+
"judgment-day",
|
|
70
|
+
// SDD phase identifiers
|
|
71
|
+
"sdd-explore",
|
|
72
|
+
"sdd-propose",
|
|
73
|
+
"sdd-spec",
|
|
74
|
+
"sdd-design",
|
|
75
|
+
"sdd-tasks",
|
|
76
|
+
"sdd-apply",
|
|
77
|
+
"sdd-verify",
|
|
78
|
+
"sdd-archive",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Supported events (mirrors defaultRuleSet.Events in triggers.go)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const SUPPORTED_EVENTS: ReadonlySet<TriggerEvent> = new Set([
|
|
86
|
+
"pre-commit",
|
|
87
|
+
"pre-push",
|
|
88
|
+
"pre-pr",
|
|
89
|
+
"post-sdd-phase",
|
|
90
|
+
"on-ci",
|
|
91
|
+
"on-schedule",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Valid SDD phase identifiers for the When.phases field.
|
|
96
|
+
// Mirrors validSDDPhases in ValidateTriggerRuleSet.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
const VALID_SDD_PHASES: ReadonlySet<string> = new Set([
|
|
100
|
+
"sdd-explore",
|
|
101
|
+
"sdd-propose",
|
|
102
|
+
"sdd-spec",
|
|
103
|
+
"sdd-design",
|
|
104
|
+
"sdd-tasks",
|
|
105
|
+
"sdd-apply",
|
|
106
|
+
"sdd-verify",
|
|
107
|
+
"sdd-archive",
|
|
108
|
+
// Short names used in post-sdd-phase conditions.
|
|
109
|
+
"explore",
|
|
110
|
+
"propose",
|
|
111
|
+
"spec",
|
|
112
|
+
"design",
|
|
113
|
+
"tasks",
|
|
114
|
+
"apply",
|
|
115
|
+
"verify",
|
|
116
|
+
"archive",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// DEFAULT_RULE_SET
|
|
121
|
+
// Ported 1:1 from triggers.go defaultRuleSet.Bindings
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export const DEFAULT_RULE_SET: TriggerRuleSet = {
|
|
125
|
+
bindings: [
|
|
126
|
+
{
|
|
127
|
+
on: "pre-commit",
|
|
128
|
+
when: { always: true },
|
|
129
|
+
run: ["review-readability"],
|
|
130
|
+
mode: "advisory",
|
|
131
|
+
reason:
|
|
132
|
+
"everyday event → ONE cheap advisory lens (~1x); full 4R fan-out reserved for pre-pr",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
on: "pre-push",
|
|
136
|
+
when: { always: true },
|
|
137
|
+
run: ["review-readability"],
|
|
138
|
+
mode: "advisory",
|
|
139
|
+
reason:
|
|
140
|
+
"everyday event → ONE cheap advisory lens (~1x); 4R fan-out reserved for pre-pr on hot paths / large diffs",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
on: "pre-pr",
|
|
144
|
+
when: {
|
|
145
|
+
pathGlobs: ["**/auth/**", "**/update/**", "**/security/**", "**/payments/**"],
|
|
146
|
+
minDiffLines: LARGE_CHANGED_LINE_THRESHOLD,
|
|
147
|
+
combine: "or",
|
|
148
|
+
},
|
|
149
|
+
run: ["review-risk", "review-resilience", "review-readability", "review-reliability"],
|
|
150
|
+
mode: "strong",
|
|
151
|
+
reason:
|
|
152
|
+
"full 4R fan-out (~4x) only on hot paths (auth/update/security/payments) or diffs exceeding 400 changed lines",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
on: "post-sdd-phase",
|
|
156
|
+
when: { phases: ["design", "apply"] },
|
|
157
|
+
run: ["judgment-day"],
|
|
158
|
+
mode: "strong",
|
|
159
|
+
reason:
|
|
160
|
+
"adversarial verification (~4 + 3*findings cost) only at high-stakes SDD phases (design and apply)",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Validation
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reports whether run contains all four 4R review agents.
|
|
171
|
+
* Mirrors has4RFanOut in triggers.go.
|
|
172
|
+
*/
|
|
173
|
+
function has4RFanOut(run: readonly string[]): boolean {
|
|
174
|
+
const found = new Set(run);
|
|
175
|
+
return (
|
|
176
|
+
found.has("review-risk") &&
|
|
177
|
+
found.has("review-readability") &&
|
|
178
|
+
found.has("review-reliability") &&
|
|
179
|
+
found.has("review-resilience")
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validates each binding in set against the closed vocabularies.
|
|
185
|
+
* Throws a descriptive Error on the first violation.
|
|
186
|
+
* Mirrors ValidateTriggerRuleSet in triggers.go.
|
|
187
|
+
*/
|
|
188
|
+
export function validateTriggerRuleSet(set: TriggerRuleSet): void {
|
|
189
|
+
const knownAgentsSet = new Set(KNOWN_AGENTS);
|
|
190
|
+
const validCombine: ReadonlySet<string> = new Set(["", "or", "and"]);
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < set.bindings.length; i++) {
|
|
193
|
+
const b = set.bindings[i];
|
|
194
|
+
|
|
195
|
+
// Validate On.
|
|
196
|
+
if (!SUPPORTED_EVENTS.has(b.on)) {
|
|
197
|
+
throw new Error(`binding[${i}]: unknown event "${b.on}"`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate Run.
|
|
201
|
+
if (!b.run || b.run.length === 0) {
|
|
202
|
+
throw new Error(`binding[${i}]: Run must not be empty`);
|
|
203
|
+
}
|
|
204
|
+
for (const agent of b.run) {
|
|
205
|
+
if (!knownAgentsSet.has(agent)) {
|
|
206
|
+
throw new Error(`binding[${i}]: unknown run agent "${agent}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate Mode.
|
|
211
|
+
if (b.mode !== "advisory" && b.mode !== "strong") {
|
|
212
|
+
throw new Error(`binding[${i}]: unknown mode "${b.mode}"`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate When vocabulary.
|
|
216
|
+
const w = b.when;
|
|
217
|
+
|
|
218
|
+
// MinDiffLines when non-zero must be positive (> 0). Zero is unset/unused; negative rejected.
|
|
219
|
+
// Check this BEFORE the "at least one condition" check so negative values get the right error.
|
|
220
|
+
if (w.minDiffLines !== undefined && w.minDiffLines < 0) {
|
|
221
|
+
throw new Error(`binding[${i}]: When.MinDiffLines must be a positive integer (> 0)`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// PathGlobs non-nil but empty is invalid.
|
|
225
|
+
if (w.pathGlobs !== undefined && w.pathGlobs.length === 0) {
|
|
226
|
+
throw new Error(`binding[${i}]: When.pathGlobs must not be an empty slice`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Must have at least one condition set.
|
|
230
|
+
const hasCondition =
|
|
231
|
+
w.always === true ||
|
|
232
|
+
(w.pathGlobs !== undefined && w.pathGlobs.length > 0) ||
|
|
233
|
+
(w.minDiffLines !== undefined && w.minDiffLines > 0) ||
|
|
234
|
+
(w.phases !== undefined && w.phases.length > 0);
|
|
235
|
+
if (!hasCondition) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`binding[${i}]: When must have at least one condition (always, pathGlobs, minDiffLines, or phases)`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Combine must be a recognized value.
|
|
242
|
+
const combineVal: string = w.combine ?? "";
|
|
243
|
+
if (!validCombine.has(combineVal)) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`binding[${i}]: When.combine "${combineVal}" is not in {"" "or" "and"}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Phases must be recognized SDD phase identifiers.
|
|
250
|
+
if (w.phases) {
|
|
251
|
+
for (const p of w.phases) {
|
|
252
|
+
if (!VALID_SDD_PHASES.has(p)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`binding[${i}]: When.phases entry "${p}" is not a recognized SDD phase identifier`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Phases is only valid for post-sdd-phase event.
|
|
261
|
+
if (w.phases && w.phases.length > 0 && b.on !== "post-sdd-phase") {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`binding[${i}]: When.phases may only be used with the post-sdd-phase event (got "${b.on}")`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Spec G prohibition: full 4R fan-out on everyday event with always=true is PROHIBITED.
|
|
268
|
+
if ((b.on === "pre-commit" || b.on === "pre-push") && w.always === true) {
|
|
269
|
+
if (has4RFanOut(b.run)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`binding[${i}]: full 4R fan-out (review-risk, review-readability, review-reliability, review-resilience) ` +
|
|
272
|
+
`on "${b.on}" with when.always=true is prohibited — everyday events must use a single advisory lens, ` +
|
|
273
|
+
`not the full 4R fan-out (spec G token-budget rule)`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate DEFAULT_RULE_SET at module load — proves it's always valid.
|
|
281
|
+
validateTriggerRuleSet(DEFAULT_RULE_SET);
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// matchPathGlobs
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Converts a glob pattern (using ** and *) to a RegExp.
|
|
289
|
+
* Supports the doublestar/segment forms used by the trigger rule set.
|
|
290
|
+
*
|
|
291
|
+
* Key behavior: a leading doublestar-slash means "zero or more leading path
|
|
292
|
+
* segments", so a pattern like auth-glob matches both "src/auth/login.ts"
|
|
293
|
+
* AND "auth/login.ts" (zero leading segments). A doublestar in a non-leading
|
|
294
|
+
* position expands to ".*". A single star expands to "[^/]*" (no separator
|
|
295
|
+
* crossing). All other regex metacharacters are escaped.
|
|
296
|
+
*/
|
|
297
|
+
function globToRegExp(glob: string): RegExp {
|
|
298
|
+
// Step 1: escape all regex metacharacters except * (which we handle below).
|
|
299
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
300
|
+
|
|
301
|
+
// Step 2: tokenize every ** before touching single *.
|
|
302
|
+
const tokenized = escaped.replace(/\*\*/g, "__DS__");
|
|
303
|
+
|
|
304
|
+
// Step 3: replace single * with [^/]* (no path-separator crossing).
|
|
305
|
+
const withSingleStar = tokenized.replace(/\*/g, "[^/]*");
|
|
306
|
+
|
|
307
|
+
// Step 4: convert a leading __DS__/ to "zero or more leading path segments",
|
|
308
|
+
// so that a glob like the auth hot-path pattern matches both
|
|
309
|
+
// "src/auth/login.ts" (leading segments) AND "auth/login.ts" (zero leading).
|
|
310
|
+
const withLeading = withSingleStar.replace(/^__DS__\//, "(?:.*/)?");
|
|
311
|
+
|
|
312
|
+
// Step 5: restore remaining __DS__ tokens as .* (match any chars including /).
|
|
313
|
+
const withDoubleStar = withLeading.replace(/__DS__/g, ".*");
|
|
314
|
+
|
|
315
|
+
return new RegExp(`^${withDoubleStar}$`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns true if any path in `paths` matches any glob in `globs`.
|
|
320
|
+
* Supports the `**` wildcard matching any path segment.
|
|
321
|
+
*/
|
|
322
|
+
export function matchPathGlobs(paths: readonly string[], globs: readonly string[]): boolean {
|
|
323
|
+
if (paths.length === 0 || globs.length === 0) return false;
|
|
324
|
+
const regexps = globs.map(globToRegExp);
|
|
325
|
+
return paths.some((p) => regexps.some((re) => re.test(p)));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// evaluateEvent
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Evaluates a trigger event against the DEFAULT_RULE_SET and the provided diff.
|
|
334
|
+
*
|
|
335
|
+
* Returns `{ run, mode, reason }` for the first binding that fires, or `null`
|
|
336
|
+
* if no binding fires.
|
|
337
|
+
*
|
|
338
|
+
* Note: `post-sdd-phase` bindings use `phases` for firing, not diff conditions.
|
|
339
|
+
* Passing a `post-sdd-phase` event here will always return null because the
|
|
340
|
+
* phase parameter is not available in this diff-based entry point. Use
|
|
341
|
+
* `evaluatePostSddPhaseEvent` for phase-driven triggering.
|
|
342
|
+
*/
|
|
343
|
+
export function evaluateEvent(
|
|
344
|
+
event: TriggerEvent,
|
|
345
|
+
diff: ChangedDiff,
|
|
346
|
+
): { run: string[]; mode: TriggerMode; reason: string } | null {
|
|
347
|
+
for (const binding of DEFAULT_RULE_SET.bindings) {
|
|
348
|
+
if (binding.on !== event) continue;
|
|
349
|
+
|
|
350
|
+
const w = binding.when;
|
|
351
|
+
|
|
352
|
+
// always → unconditional match
|
|
353
|
+
if (w.always === true) {
|
|
354
|
+
return { run: binding.run, mode: binding.mode, reason: binding.reason };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// post-sdd-phase uses phases, not diff conditions — skip here
|
|
358
|
+
if (event === "post-sdd-phase") {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Evaluate path and line conditions using combine mode
|
|
363
|
+
const combine = w.combine ?? "or";
|
|
364
|
+
const pathMatches =
|
|
365
|
+
w.pathGlobs && w.pathGlobs.length > 0
|
|
366
|
+
? matchPathGlobs(diff.changedPaths, w.pathGlobs)
|
|
367
|
+
: false;
|
|
368
|
+
const lineMatches =
|
|
369
|
+
w.minDiffLines !== undefined && w.minDiffLines > 0
|
|
370
|
+
? diff.changedLines >= w.minDiffLines
|
|
371
|
+
: false;
|
|
372
|
+
|
|
373
|
+
const hasPathCondition = w.pathGlobs !== undefined && w.pathGlobs.length > 0;
|
|
374
|
+
const hasLineCondition = w.minDiffLines !== undefined && w.minDiffLines > 0;
|
|
375
|
+
|
|
376
|
+
let fires = false;
|
|
377
|
+
if (combine === "and") {
|
|
378
|
+
// Both conditions must hold (only when both are specified)
|
|
379
|
+
if (hasPathCondition && hasLineCondition) {
|
|
380
|
+
fires = pathMatches && lineMatches;
|
|
381
|
+
} else if (hasPathCondition) {
|
|
382
|
+
fires = pathMatches;
|
|
383
|
+
} else if (hasLineCondition) {
|
|
384
|
+
fires = lineMatches;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// "or" or "" — any condition firing is enough
|
|
388
|
+
fires = pathMatches || lineMatches;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (fires) {
|
|
392
|
+
return { run: binding.run, mode: binding.mode, reason: binding.reason };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Evaluates a post-sdd-phase trigger for a specific SDD phase name.
|
|
401
|
+
* Returns `{ run, mode, reason }` if a binding matches, or `null`.
|
|
402
|
+
*/
|
|
403
|
+
export function evaluatePostSddPhaseEvent(
|
|
404
|
+
phase: string,
|
|
405
|
+
): { run: string[]; mode: TriggerMode; reason: string } | null {
|
|
406
|
+
for (const binding of DEFAULT_RULE_SET.bindings) {
|
|
407
|
+
if (binding.on !== "post-sdd-phase") continue;
|
|
408
|
+
const w = binding.when;
|
|
409
|
+
if (w.phases && w.phases.includes(phase)) {
|
|
410
|
+
return { run: binding.run, mode: binding.mode, reason: binding.reason };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -84,6 +84,33 @@ test("rendered SDD preflight prompt is English artifact copy", () => {
|
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
test("orchestrator Memory Contract carries the Engram memory lifecycle rule", async () => {
|
|
88
|
+
const orchestrator = await readFile(join(ROOT, "assets/orchestrator.md"), "utf8");
|
|
89
|
+
|
|
90
|
+
// Mirrors gentle-ai's engram-protocol/engram-convention lifecycle rule (PRs #842 + #844),
|
|
91
|
+
// in its final availability-gated form: agents must treat needs_review memories as stale,
|
|
92
|
+
// prefer mem_review when present, fall back safely when it is not, and never auto-mark reviewed.
|
|
93
|
+
for (const required of [
|
|
94
|
+
"when Engram exposes lifecycle metadata/tooling",
|
|
95
|
+
"At session start or before architecture-sensitive work",
|
|
96
|
+
"call `mem_review` with action `list`",
|
|
97
|
+
"for the current project when the tool is available",
|
|
98
|
+
"If `mem_review` is unavailable, do not fail the task",
|
|
99
|
+
"Continue with normal `mem_context`/`mem_search`",
|
|
100
|
+
"still apply lifecycle metadata from any returned observations when present",
|
|
101
|
+
"`active` memories may be used normally",
|
|
102
|
+
"`needs_review` memories are stale context, not trusted facts",
|
|
103
|
+
"verify it against current evidence before relying on it",
|
|
104
|
+
"Do NOT call `mem_review` with action `mark_reviewed` automatically",
|
|
105
|
+
"Only call `mark_reviewed` after explicit user confirmation or through a dedicated memory maintenance command",
|
|
106
|
+
]) {
|
|
107
|
+
assert.ok(
|
|
108
|
+
orchestrator.includes(required),
|
|
109
|
+
`orchestrator.md missing memory lifecycle rule: ${required}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
87
114
|
test("SDD proposal questions focus on business and PRD gaps", async () => {
|
|
88
115
|
const proposalAgent = await readFile(join(ROOT, "assets/agents/sdd-proposal.md"), "utf8");
|
|
89
116
|
|