peaks-loop 4.0.52 → 4.0.54
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/CHANGELOG.md +89 -2
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/codegraph-commands.js +164 -5
- package/dist/cli/commands/core/memory-command.js +35 -2
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/prd-commands.js +15 -0
- package/dist/cli/commands/sub-agent-shared.d.ts +23 -0
- package/dist/cli/commands/sub-agent-shared.js +39 -0
- package/dist/services/artifacts/artifact-prerequisites.js +34 -0
- package/dist/services/audit/enforcer-liveness.js +1 -1
- package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +44 -0
- package/dist/services/codegraph/codegraph-config-repair-writer.js +112 -13
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +23 -1
- package/dist/services/codegraph/codegraph-exclude-repair.js +6 -0
- package/dist/services/dispatch/sub-agent-dispatcher.d.ts +0 -1
- package/dist/services/dispatch/sub-agent-dispatcher.js +0 -3
- package/dist/services/evidence/evidence-generator.js +11 -3
- package/dist/services/memory/project-memory-service/index.d.ts +1 -1
- package/dist/services/memory/project-memory-service/index.js +1 -1
- package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +59 -0
- package/dist/services/memory/project-memory-service/store/atomic-write.js +167 -7
- package/dist/services/prd/gate-evidence-derivation.d.ts +160 -0
- package/dist/services/prd/gate-evidence-derivation.js +270 -0
- package/dist/services/prd/handoff-auto-regen.js +13 -1
- package/dist/services/prd/handoff-frontmatter.d.ts +1 -1
- package/dist/services/prd/handoff-frontmatter.js +62 -0
- package/dist/services/prd/handoff-gate-evidence.d.ts +95 -0
- package/dist/services/prd/handoff-gate-evidence.js +145 -0
- package/dist/services/prd/handoff-service.d.ts +21 -1
- package/dist/services/prd/handoff-service.js +45 -2
- package/dist/services/prd/handoff-types.d.ts +40 -0
- package/dist/services/prd/handoff-types.js +28 -1
- package/dist/services/prd/project-scan-reader.d.ts +7 -0
- package/dist/services/prd/project-scan-reader.js +7 -1
- package/dist/services/rd/reviewer-dispatch-policy.d.ts +36 -8
- package/dist/services/rd/reviewer-dispatch-policy.js +36 -8
- package/package.json +6 -6
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +26 -10
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +1 -1
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +7 -5
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- /package/{docs → contracts}/test-style-contract.md +0 -0
|
@@ -501,6 +501,40 @@ export async function checkPrerequisites(options) {
|
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
+
// GATE C, second half (B2, rid `rid-b2-gate-evidence-wiring`): the capsule's
|
|
505
|
+
// own `gateEvidence` DECLARATION must be true — every path it names must
|
|
506
|
+
// exist. The table loop above owns "which artifacts this type must produce";
|
|
507
|
+
// this owns "the handoff may not claim evidence it does not have". They are
|
|
508
|
+
// complementary, and neither restates the other's paths: the declaration is
|
|
509
|
+
// derived from THE SAME table (`gate-evidence-derivation.ts`), so a row
|
|
510
|
+
// changed above changes the declared path below without a second edit.
|
|
511
|
+
//
|
|
512
|
+
// Guarded on `rd:qa-handoff` because that IS Gate C (the doc's gate table is
|
|
513
|
+
// keyed on it), and placed after the loop so it inherits the same
|
|
514
|
+
// `missing` / `warnings` / `ok` arithmetic the caller already throws on —
|
|
515
|
+
// `--allow-incomplete` and the `bypassedPrerequisites` report keep working
|
|
516
|
+
// with no second bypass path to keep in sync.
|
|
517
|
+
//
|
|
518
|
+
// It is skipped for types whose table has no `rd:qa-handoff` row (docs /
|
|
519
|
+
// chore) — the early return above means those types require no evidence at
|
|
520
|
+
// all, so there is no evidence contract to check. The `projectScan` they do
|
|
521
|
+
// carry is Gate A's, and Gate A is the check that owns it.
|
|
522
|
+
//
|
|
523
|
+
// The import is dynamic because `gate-evidence-derivation.ts` reaches
|
|
524
|
+
// `handoff-service.ts`, which imports `REQUEST_ID_PATTERN` from
|
|
525
|
+
// `request-artifact-service.ts`, which imports this module: a static edge
|
|
526
|
+
// here would close a runtime import cycle. Same idiom, same reason, as
|
|
527
|
+
// `request-commands.ts`'s lazy producer imports.
|
|
528
|
+
if (options.role === 'rd' && options.newState === 'qa-handoff') {
|
|
529
|
+
const { checkDeclaredGateEvidence } = await import('../prd/gate-evidence-derivation.js');
|
|
530
|
+
const declaredEvidence = await checkDeclaredGateEvidence({
|
|
531
|
+
projectRoot: options.projectRoot,
|
|
532
|
+
sessionId: options.sessionId,
|
|
533
|
+
requestId: options.requestId
|
|
534
|
+
});
|
|
535
|
+
missing.push(...declaredEvidence.missing);
|
|
536
|
+
warnings.push(...declaredEvidence.warnings);
|
|
537
|
+
}
|
|
504
538
|
const result = { ok: missing.length === 0, missing, warnings };
|
|
505
539
|
emitPrereqTransitionEvent({
|
|
506
540
|
projectRoot: options.projectRoot,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforcer liveness — A9 of
|
|
2
|
+
* Enforcer liveness — A9 of `.peaks/docs/diagnosis-2026-09-15-peaks-loop-state.md`.
|
|
3
3
|
*
|
|
4
4
|
* `cli-backed` used to mean "the `enforcerRef` path exists on disk"
|
|
5
5
|
* (`backing-detector.ts`). It did not mean "the enforcer runs". Ten catalog
|
|
@@ -86,3 +86,47 @@ export type CodegraphConfigRepairOutcome = {
|
|
|
86
86
|
* anything is written, so a throw can never half-apply.
|
|
87
87
|
*/
|
|
88
88
|
export declare function applyCodegraphConfigRepair(projectRoot: string, repair: CodegraphConfigRepairPlan): CodegraphConfigRepairOutcome;
|
|
89
|
+
export type CodegraphConfigRollbackResult = {
|
|
90
|
+
readonly rolledBack: true;
|
|
91
|
+
readonly configPath: string;
|
|
92
|
+
readonly backupPath: string;
|
|
93
|
+
} | {
|
|
94
|
+
readonly rolledBack: false;
|
|
95
|
+
readonly configPath: string;
|
|
96
|
+
readonly backupPath: string;
|
|
97
|
+
/** Why the rollback did not happen. Never `null` on this arm. */
|
|
98
|
+
readonly error: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Put `configPath` back to the bytes `writeConfigBackup` saved beside it, and
|
|
102
|
+
* back to the mode those bytes were saved with.
|
|
103
|
+
*
|
|
104
|
+
* This is the reverse of `applyCodegraphConfigRepair`'s write half and the only
|
|
105
|
+
* reason the `.bak` exists: until this function, the backup had no reader
|
|
106
|
+
* anywhere in the repository, so "byte-exact rollback" described a file nobody
|
|
107
|
+
* ever restored.
|
|
108
|
+
*
|
|
109
|
+
* The guard is `linkOrDirectoryAt` — the SAME predicate that refuses to WRITE
|
|
110
|
+
* through a link at the backup path refuses to READ through one here. The
|
|
111
|
+
* attack it closes is the mirror image of the writer's: `config.json.bak` is
|
|
112
|
+
* fixed and committable, so a repository that ships it as a link would
|
|
113
|
+
* otherwise have the link's target's bytes published into `.codegraph/config.json`
|
|
114
|
+
* as if they were the pre-repair config — attacker-chosen content, written
|
|
115
|
+
* under the operator's own config path, from a file the operator never
|
|
116
|
+
* reviewed. A refusal is a refusal and not a repair, at both ends.
|
|
117
|
+
*
|
|
118
|
+
* The restore runs through `writeConfigAtomic` (same-directory CSPRNG temp +
|
|
119
|
+
* `renameSync`) for the same reason the backup write does: a crash mid-write
|
|
120
|
+
* must leave the repaired config intact rather than a prefix of the restored
|
|
121
|
+
* one, and the mode is applied to the TEMP before the rename so the published
|
|
122
|
+
* file never exists at the process umask. The mode comes from `statSync` of the
|
|
123
|
+
* backup, symmetrical with the write end: the copy carries the original
|
|
124
|
+
* config's mode, so handing that mode back to the config restores what the
|
|
125
|
+
* project had granted rather than this process's umask.
|
|
126
|
+
*
|
|
127
|
+
* A refusal, or a backup that cannot be read, is RETURNED rather than thrown:
|
|
128
|
+
* the caller reports it as a field and must keep going — the forced rebuild it
|
|
129
|
+
* was asked for still has to run. `writeConfigAtomic`'s own fs throw is left to
|
|
130
|
+
* propagate, and the caller catches that too.
|
|
131
|
+
*/
|
|
132
|
+
export declare function rollbackCodegraphConfig(configPath: string): Promise<CodegraphConfigRollbackResult>;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// The pure repair plans (`repairCodegraphExclude` for the exclude axis,
|
|
4
4
|
// `repairCodegraphInclude` for the include axis) and the writer that applies
|
|
5
5
|
// them to `<projectRoot>/.codegraph/config.json` in ONE atomic rewrite, plus
|
|
6
|
-
// the byte-exact `config.json.bak` copy it keeps for rollback
|
|
6
|
+
// the byte-exact `config.json.bak` copy it keeps for rollback and
|
|
7
|
+
// `rollbackCodegraphConfig`, that copy's reader (slice A1 of
|
|
8
|
+
// `2026-09-17-session-607ead`: the reverse write, so the rollback the `.bak`
|
|
9
|
+
// has always promised is reachable).
|
|
7
10
|
//
|
|
8
11
|
// Extracted verbatim from `codegraph-exclude-repair.ts` (rid
|
|
9
12
|
// 2026-09-17-oversize-followup, G1 — the 800-line file-size cap). Every moved
|
|
@@ -142,6 +145,40 @@ function writeConfigAtomic(filePath, content, mode) {
|
|
|
142
145
|
throw error;
|
|
143
146
|
}
|
|
144
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* The shared shape of the two refusals this module makes at the FIXED,
|
|
150
|
+
* therefore guessable, `config.json.bak` path: a symbolic link (bytes land in
|
|
151
|
+
* whatever it points at), a directory (there is nowhere for them to land), or
|
|
152
|
+
* a hard link (`nlink > 1` — another name shares this inode). Returns `null`
|
|
153
|
+
* when the path is absent or is a plain regular file, the only two states both
|
|
154
|
+
* ends of the rollback contract may act on.
|
|
155
|
+
*
|
|
156
|
+
* ONE predicate, two callers, and that is the point: `writeConfigBackup`
|
|
157
|
+
* WRITES this copy and `rollbackCodegraphConfig` READS it back. A guard that
|
|
158
|
+
* drifted between the two would leave the rollback trusting a path the writer
|
|
159
|
+
* would have refused, so the refusal is the same at both ends because the
|
|
160
|
+
* trust premise is the same at both ends.
|
|
161
|
+
*
|
|
162
|
+
* `lstat`, never `stat`: `stat` follows a link and reports the victim's
|
|
163
|
+
* regular-file type, which is exactly the case both callers must refuse.
|
|
164
|
+
*/
|
|
165
|
+
function linkOrDirectoryAt(path) {
|
|
166
|
+
const existing = lstatSync(path, { throwIfNoEntry: false });
|
|
167
|
+
if (existing === undefined ||
|
|
168
|
+
(!existing.isSymbolicLink() && !existing.isDirectory() && existing.nlink <= 1)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
// "through" for a link (the bytes land in whatever it points at), "at" for
|
|
173
|
+
// a directory (there is nowhere for them to land).
|
|
174
|
+
preposition: existing.isDirectory() ? 'at' : 'through',
|
|
175
|
+
kind: existing.isSymbolicLink()
|
|
176
|
+
? 'a symbolic link'
|
|
177
|
+
: existing.isDirectory()
|
|
178
|
+
? 'a directory'
|
|
179
|
+
: `a hard link (link count ${String(existing.nlink)})`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
145
182
|
/**
|
|
146
183
|
* Copy the config's ORIGINAL bytes to `config.json.bak`, refusing to write
|
|
147
184
|
* through a link that already occupies that path.
|
|
@@ -207,18 +244,9 @@ function writeConfigAtomic(filePath, content, mode) {
|
|
|
207
244
|
*/
|
|
208
245
|
function writeConfigBackup(configPath, originalText) {
|
|
209
246
|
const backupPath = `${configPath}${CODEGRAPH_CONFIG_BACKUP_SUFFIX}`;
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
const kind = existing.isSymbolicLink()
|
|
214
|
-
? 'a symbolic link'
|
|
215
|
-
: existing.isDirectory()
|
|
216
|
-
? 'a directory'
|
|
217
|
-
: `a hard link (link count ${String(existing.nlink)})`;
|
|
218
|
-
// "through" for a link (the bytes land in whatever it points at), "at" for
|
|
219
|
-
// a directory (there is nowhere for them to land).
|
|
220
|
-
const preposition = existing.isDirectory() ? 'at' : 'through';
|
|
221
|
-
throw new Error(`codegraph config backup ${backupPath}: refusing to write ${preposition} ${kind} occupying ` +
|
|
247
|
+
const occupied = linkOrDirectoryAt(backupPath);
|
|
248
|
+
if (occupied !== null) {
|
|
249
|
+
throw new Error(`codegraph config backup ${backupPath}: refusing to write ${occupied.preposition} ${occupied.kind} occupying ` +
|
|
222
250
|
'this path. Another file or directory shares it, so a backup written here would overwrite ' +
|
|
223
251
|
'that. Remove it (or point `peaks` at a project root whose `.codegraph/` it owns) and re-run.');
|
|
224
252
|
}
|
|
@@ -320,3 +348,74 @@ export function applyCodegraphConfigRepair(projectRoot, repair) {
|
|
|
320
348
|
includeCountAfter: includePlan.include.length
|
|
321
349
|
};
|
|
322
350
|
}
|
|
351
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Rollback — the read side of the `.bak`
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
354
|
+
/** Local, because this module must not import `codegraph-exclude-repair.ts`. */
|
|
355
|
+
function errorMessage(error) {
|
|
356
|
+
return error instanceof Error ? error.message : String(error);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Put `configPath` back to the bytes `writeConfigBackup` saved beside it, and
|
|
360
|
+
* back to the mode those bytes were saved with.
|
|
361
|
+
*
|
|
362
|
+
* This is the reverse of `applyCodegraphConfigRepair`'s write half and the only
|
|
363
|
+
* reason the `.bak` exists: until this function, the backup had no reader
|
|
364
|
+
* anywhere in the repository, so "byte-exact rollback" described a file nobody
|
|
365
|
+
* ever restored.
|
|
366
|
+
*
|
|
367
|
+
* The guard is `linkOrDirectoryAt` — the SAME predicate that refuses to WRITE
|
|
368
|
+
* through a link at the backup path refuses to READ through one here. The
|
|
369
|
+
* attack it closes is the mirror image of the writer's: `config.json.bak` is
|
|
370
|
+
* fixed and committable, so a repository that ships it as a link would
|
|
371
|
+
* otherwise have the link's target's bytes published into `.codegraph/config.json`
|
|
372
|
+
* as if they were the pre-repair config — attacker-chosen content, written
|
|
373
|
+
* under the operator's own config path, from a file the operator never
|
|
374
|
+
* reviewed. A refusal is a refusal and not a repair, at both ends.
|
|
375
|
+
*
|
|
376
|
+
* The restore runs through `writeConfigAtomic` (same-directory CSPRNG temp +
|
|
377
|
+
* `renameSync`) for the same reason the backup write does: a crash mid-write
|
|
378
|
+
* must leave the repaired config intact rather than a prefix of the restored
|
|
379
|
+
* one, and the mode is applied to the TEMP before the rename so the published
|
|
380
|
+
* file never exists at the process umask. The mode comes from `statSync` of the
|
|
381
|
+
* backup, symmetrical with the write end: the copy carries the original
|
|
382
|
+
* config's mode, so handing that mode back to the config restores what the
|
|
383
|
+
* project had granted rather than this process's umask.
|
|
384
|
+
*
|
|
385
|
+
* A refusal, or a backup that cannot be read, is RETURNED rather than thrown:
|
|
386
|
+
* the caller reports it as a field and must keep going — the forced rebuild it
|
|
387
|
+
* was asked for still has to run. `writeConfigAtomic`'s own fs throw is left to
|
|
388
|
+
* propagate, and the caller catches that too.
|
|
389
|
+
*/
|
|
390
|
+
export async function rollbackCodegraphConfig(configPath) {
|
|
391
|
+
const backupPath = `${configPath}${CODEGRAPH_CONFIG_BACKUP_SUFFIX}`;
|
|
392
|
+
const occupied = linkOrDirectoryAt(backupPath);
|
|
393
|
+
if (occupied !== null) {
|
|
394
|
+
return {
|
|
395
|
+
rolledBack: false,
|
|
396
|
+
configPath,
|
|
397
|
+
backupPath,
|
|
398
|
+
error: `codegraph config rollback ${backupPath}: refusing to restore ${occupied.preposition} ${occupied.kind} occupying ` +
|
|
399
|
+
'this path. A backup this writer did not create is not a rollback point, and restoring through it ' +
|
|
400
|
+
'would publish bytes nobody reviewed. Restore it by hand and re-run.'
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
let originalText;
|
|
404
|
+
let originalMode;
|
|
405
|
+
try {
|
|
406
|
+
originalText = readFileSync(backupPath, 'utf8');
|
|
407
|
+
// `& 0o777` drops the file-type bits `statSync` packs above the permission
|
|
408
|
+
// bits — `chmod` takes permission bits only.
|
|
409
|
+
originalMode = statSync(backupPath).mode & 0o777;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
return {
|
|
413
|
+
rolledBack: false,
|
|
414
|
+
configPath,
|
|
415
|
+
backupPath,
|
|
416
|
+
error: `codegraph config rollback: cannot read ${backupPath}: ${errorMessage(error)}`
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
writeConfigAtomic(configPath, originalText, originalMode);
|
|
420
|
+
return { rolledBack: true, configPath, backupPath };
|
|
421
|
+
}
|
|
@@ -64,7 +64,15 @@ export type CodegraphExcludeRepairReport = {
|
|
|
64
64
|
* AND the reads failed (there is no config to name).
|
|
65
65
|
*/
|
|
66
66
|
readonly configPath: string;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Byte-exact rollback copy (null when nothing was written).
|
|
69
|
+
*
|
|
70
|
+
* It is a rollback POINT, not a rollback: nothing in this seam reads it back.
|
|
71
|
+
* Putting the config back is the separate, explicit
|
|
72
|
+
* `peaks codegraph config-restore` verb, so this field is what that verb
|
|
73
|
+
* would read — and its presence is the one guarantee that a restore is
|
|
74
|
+
* possible at all.
|
|
75
|
+
*/
|
|
68
76
|
readonly backupPath: string | null;
|
|
69
77
|
/** True when the post-repair `codegraph index` finished successfully. */
|
|
70
78
|
readonly reindexed: boolean;
|
|
@@ -127,6 +135,20 @@ export type CodegraphExcludeRepairOptions = {
|
|
|
127
135
|
* C) exists so that upgrading peaks-loop cannot change a downstream
|
|
128
136
|
* project's cost profile, and purging dead rows is the caller's explicit
|
|
129
137
|
* request instead.
|
|
138
|
+
*
|
|
139
|
+
* `'force'` changes ONLY the rebuild, never the config: it is the same
|
|
140
|
+
* two-axis repair as `'exclude'`, followed by `cg.clear()` + `indexAll()`
|
|
141
|
+
* instead of an incremental index. The config repair STAYS — that is what
|
|
142
|
+
* makes the force mode worth its cost, because `include` has to be widened
|
|
143
|
+
* before a rebuild can admit the files the config was dropping.
|
|
144
|
+
*
|
|
145
|
+
* Not a self-cancelling run, and that is a design decision rather than a
|
|
146
|
+
* detail: a mode that wrote the repair and then restored the pre-repair bytes
|
|
147
|
+
* would leave the config exactly as it found it, `peaks codegraph status`
|
|
148
|
+
* would still report the gap, and exit 75 would never clear. Putting the
|
|
149
|
+
* config BACK is the explicit `peaks codegraph config-restore` verb, which
|
|
150
|
+
* reads the `.bak` this run leaves — an operator decision, not a side effect
|
|
151
|
+
* of asking for a rebuild.
|
|
130
152
|
*/
|
|
131
153
|
readonly reindex?: boolean | 'force';
|
|
132
154
|
};
|
|
@@ -64,6 +64,12 @@
|
|
|
64
64
|
// consumer project (`config.json.bak` matches neither the peaks-loop
|
|
65
65
|
// snippet nor upstream's own `.codegraph/.gitignore`), so both files
|
|
66
66
|
// are committable.
|
|
67
|
+
// - `rollbackCodegraphConfig` is that copy's READER, and it is reached by the
|
|
68
|
+
// EXPLICIT `peaks codegraph config-restore` verb — never from here. A
|
|
69
|
+
// repair must not undo itself: this seam's job is to close the gap, so a
|
|
70
|
+
// run that restored its own write would leave the config exactly as it
|
|
71
|
+
// found it and `peaks codegraph status` still reporting the gap (exit 75
|
|
72
|
+
// never clearing). Rolling back is an operator decision, not a step.
|
|
67
73
|
// - The DIRECTORY both paths live in is contained: `assertCodegraphDirContained`
|
|
68
74
|
// refuses when `<projectRoot>/.codegraph` resolves (junction or symlink)
|
|
69
75
|
// outside the canonical project root, so neither the read nor either
|
|
@@ -207,7 +207,6 @@ export declare function awaitClaudeCodeBatch(input: SubAgentAwaitBatchInput): Pr
|
|
|
207
207
|
* `awaitBatch` is a real implementation.
|
|
208
208
|
*/
|
|
209
209
|
export interface PollDispatchRecordsOptions {
|
|
210
|
-
readonly ide: 'trae' | 'codex' | 'cursor';
|
|
211
210
|
readonly defaultTimeoutMs: number;
|
|
212
211
|
readonly notePrefix: string;
|
|
213
212
|
}
|
|
@@ -101,7 +101,6 @@ export const traeSubAgentDispatcher = {
|
|
|
101
101
|
// Trae-default heartbeat (30s). The fallback `awaitByLlm` marker is
|
|
102
102
|
// gone — Trae now joins like claude-code.
|
|
103
103
|
awaitBatch: async (input) => pollDispatchRecords(input, {
|
|
104
|
-
ide: 'trae',
|
|
105
104
|
defaultTimeoutMs: 30_000,
|
|
106
105
|
notePrefix: 'trae 1.3 real awaitBatch'
|
|
107
106
|
})
|
|
@@ -129,7 +128,6 @@ export const codexSubAgentDispatcher = {
|
|
|
129
128
|
toolCallVersion: '2.0.0',
|
|
130
129
|
}),
|
|
131
130
|
awaitBatch: async (input) => pollDispatchRecords(input, {
|
|
132
|
-
ide: 'codex',
|
|
133
131
|
defaultTimeoutMs: 45_000,
|
|
134
132
|
notePrefix: 'codex 1.3 real awaitBatch'
|
|
135
133
|
})
|
|
@@ -155,7 +153,6 @@ export const cursorSubAgentDispatcher = {
|
|
|
155
153
|
toolCallVersion: '2.0.0',
|
|
156
154
|
}),
|
|
157
155
|
awaitBatch: async (input) => pollDispatchRecords(input, {
|
|
158
|
-
ide: 'cursor',
|
|
159
156
|
defaultTimeoutMs: 30_000,
|
|
160
157
|
notePrefix: 'cursor 1.3 real awaitBatch'
|
|
161
158
|
})
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { serializeHandoffFrontmatter } from '../prd/handoff-frontmatter.js';
|
|
34
|
+
import { deriveGateEvidenceForRequest } from '../prd/gate-evidence-derivation.js';
|
|
34
35
|
import { handoffRelativePath, sha256OfBody } from '../prd/handoff-service.js';
|
|
35
36
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
36
37
|
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
@@ -260,7 +261,7 @@ function buildQaRequest(rid, sid, files) {
|
|
|
260
261
|
* producer/consumer divergence rid `2026-09-14-handoff-writer-gate-divergence`
|
|
261
262
|
* exists to remove, surviving in a function the same slice edited.
|
|
262
263
|
*/
|
|
263
|
-
function buildHandoff(rid, sid, title, files, lineCounts) {
|
|
264
|
+
function buildHandoff(rid, sid, title, files, lineCounts, gateEvidence) {
|
|
264
265
|
const body = `# PRD Handoff — ${rid}
|
|
265
266
|
|
|
266
267
|
${title}. Mechanical verbatim module split; behavior-preserving.
|
|
@@ -281,7 +282,13 @@ ${lineCountsMd(lineCounts)}
|
|
|
281
282
|
goals: [],
|
|
282
283
|
acceptanceCriteria: [],
|
|
283
284
|
preservedBehavior: [],
|
|
284
|
-
handoffPath: handoffRelativePath(sid, rid)
|
|
285
|
+
handoffPath: handoffRelativePath(sid, rid),
|
|
286
|
+
// B2 / F1: the THIRD frontmatter producer. B1 left this one untouched and
|
|
287
|
+
// QA found it, so a `peaks evidence generate` capsule and a
|
|
288
|
+
// `peaks prd handoff init` capsule for the same slice disagreed about the
|
|
289
|
+
// same contract. Passed in rather than derived here so this function stays
|
|
290
|
+
// synchronous and disk-free; the caller derives.
|
|
291
|
+
...(gateEvidence === undefined ? {} : { gateEvidence })
|
|
285
292
|
};
|
|
286
293
|
return { content: `${serializeHandoffFrontmatter(frontmatter)}${body}`, hash: handoffHash };
|
|
287
294
|
}
|
|
@@ -334,7 +341,8 @@ export async function generateEvidence(options) {
|
|
|
334
341
|
createdDirectories.push(dir);
|
|
335
342
|
}
|
|
336
343
|
const qaRequestPath = await resolveQaRequestPath(qaDir, rid);
|
|
337
|
-
const
|
|
344
|
+
const gateEvidence = await deriveGateEvidenceForRequest({ projectRoot, sessionId, requestId: rid });
|
|
345
|
+
const handoff = buildHandoff(rid, sessionId, title, files, lineCounts, gateEvidence);
|
|
338
346
|
// Four of these twelve paths carry the rid because the `rd:qa-handoff` gate
|
|
339
347
|
// requires `<rid>` in them: every slice in a session shares `rd/` and
|
|
340
348
|
// `audit/`, so writing the ridless name silently destroyed the previous
|
|
@@ -7,7 +7,7 @@ export type { ExtractedMemoryBlocks } from './parsers/markdown-pure.js';
|
|
|
7
7
|
export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
|
|
8
8
|
export type { MemoryKindResolution, MemoryKindSource, MemoryNameResolution, MemoryNameSource, ParsedMemoryFrontmatter } from './parsers/frontmatter.js';
|
|
9
9
|
export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
|
|
10
|
-
export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
|
|
10
|
+
export { assertSafeMemory, assertSafeMemoryFileContent, findSensitiveMemoryTitleTerm, hasSensitiveMemoryContent, SENSITIVE_MEMORY_CHECKS, UnsafeMemoryError, writeNewFile } from './store/atomic-write.js';
|
|
11
11
|
export { ensureMemoryBootstrap, emptyByKind, emptyIndex, listMarkdownFiles, readProjectMemories, readProjectMemoryBody } from './index/search.js';
|
|
12
12
|
export { buildMemoryIndex, generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
13
13
|
export { executeMemoryReindex, renderMemoryMarkdown, KIND_ORDER, MEMORY_MD_BANNER, MEMORY_MD_FILENAME } from './index/reindex.js';
|
|
@@ -19,7 +19,7 @@ export { describeMemoryBlockDrops, describeSessionScanFailures, summarizeBackupR
|
|
|
19
19
|
export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
|
|
20
20
|
// Store: path safety + sensitive content
|
|
21
21
|
export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
|
|
22
|
-
export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
|
|
22
|
+
export { assertSafeMemory, assertSafeMemoryFileContent, findSensitiveMemoryTitleTerm, hasSensitiveMemoryContent, SENSITIVE_MEMORY_CHECKS, UnsafeMemoryError, writeNewFile } from './store/atomic-write.js';
|
|
23
23
|
// Index: search / ranking / dispatch
|
|
24
24
|
export { ensureMemoryBootstrap, emptyByKind, emptyIndex, listMarkdownFiles, readProjectMemories, readProjectMemoryBody } from './index/search.js';
|
|
25
25
|
export { buildMemoryIndex, generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
import type { ExtractedProjectMemory } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The names of the three checks `assertSafeMemory` can fail on, written once.
|
|
4
|
+
*
|
|
5
|
+
* The name of the check that fired rides the refusal message, so the reader is
|
|
6
|
+
* told WHICH rule stopped the write instead of only that something was
|
|
7
|
+
* refused. Callers route their remedy on these same constants (see
|
|
8
|
+
* `memoryExtractNextActions` in `src/cli/commands/core/memory-command.ts`) —
|
|
9
|
+
* two strings kept equal by hand is how a message and its advice drift apart.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SENSITIVE_MEMORY_CHECKS: {
|
|
12
|
+
readonly metadataKey: "metadata key scan";
|
|
13
|
+
readonly content: "content scan";
|
|
14
|
+
readonly title: "title scan";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The credential term this title contains (as a run of words), or `null`.
|
|
18
|
+
*
|
|
19
|
+
* Returns the MATCHED TERM rather than a boolean because the refusal message
|
|
20
|
+
* names it: `title scan matched the credential term "apikey"` is actionable,
|
|
21
|
+
* while "Refusing to store sensitive memory content" is what sent slice C0's
|
|
22
|
+
* user looking for a secret that was not there. The returned string is a
|
|
23
|
+
* dictionary word from the list above — never the title's own text, and never
|
|
24
|
+
* a value — so echoing it into the error envelope cannot leak anything.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findSensitiveMemoryTitleTerm(title: string): string | null;
|
|
2
27
|
export declare function hasSensitiveMemoryContent(content: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The refusal `assertSafeMemory` raises: the check that fired and the term it
|
|
30
|
+
* matched travel as DATA, not only as prose.
|
|
31
|
+
*
|
|
32
|
+
* WHY A TYPE RATHER THAN A MESSAGE. The envelope redactor behind every
|
|
33
|
+
* `fail()` (`packages/peaks-loop-shared/src/result.ts`) strips the words
|
|
34
|
+
* secret / token / password / api-key out of every failure MESSAGE — a blanket
|
|
35
|
+
* rule, applied to messages this module does not own. So a message that names
|
|
36
|
+
* the matched term reaches the CLI reading `… the credential term
|
|
37
|
+
* "[redacted]" …`: the term is named, and then removed, on the way out.
|
|
38
|
+
*
|
|
39
|
+
* The term is a word from `SENSITIVE_PROSE_TERMS` — a fixed vocabulary, never a
|
|
40
|
+
* value — so there is nothing about it to redact, and it rides a field
|
|
41
|
+
* instead: `fail()` redacts `message` only. The CLI routes its remedy on
|
|
42
|
+
* `check` for the same reason — a substring test against a message whose words
|
|
43
|
+
* can be rewritten is not a routing rule, it is a guess.
|
|
44
|
+
*
|
|
45
|
+
* NOTHING HERE WEAKENS THE REDACTOR. The content scan never puts its match in
|
|
46
|
+
* this object (see `matchedTerm` below), so no value can reach an envelope
|
|
47
|
+
* through it; the redactor's guarantee is intact and is pinned by the cases in
|
|
48
|
+
* `tests/unit/services/memory/memory-title-sensitive-scan.test.ts`.
|
|
49
|
+
*/
|
|
50
|
+
export declare class UnsafeMemoryError extends Error {
|
|
51
|
+
/** One of `SENSITIVE_MEMORY_CHECKS` — which rule refused the write. */
|
|
52
|
+
readonly check: string;
|
|
53
|
+
/**
|
|
54
|
+
* The credential term the TITLE scan matched, or `null` for the other two
|
|
55
|
+
* checks. `null` is not an omission: their match can BE the credential
|
|
56
|
+
* (`ghp_…`, the PEM header, the JWT), which is why their messages describe
|
|
57
|
+
* the pattern family and never echo the text.
|
|
58
|
+
*/
|
|
59
|
+
readonly matchedTerm: string | null;
|
|
60
|
+
constructor(check: string, detail: string, matchedTerm?: string | null);
|
|
61
|
+
}
|
|
3
62
|
export declare function assertSafeMemory(memory: ExtractedProjectMemory): void;
|
|
4
63
|
export declare function assertSafeMemoryFileContent(content: string): void;
|
|
5
64
|
export declare function writeNewFile(path: string, content: string): void;
|