peaks-loop 4.0.48 → 4.0.50
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 +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -27,6 +27,8 @@ import { existsSync } from 'node:fs';
|
|
|
27
27
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
28
28
|
import { dirname, join } from 'node:path';
|
|
29
29
|
import { parse as parseYaml } from 'yaml';
|
|
30
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
31
|
+
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
30
32
|
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
31
33
|
/** Required schema version for new handoffs. */
|
|
32
34
|
const HANDOFF_SCHEMA_VERSION = '2';
|
|
@@ -34,6 +36,39 @@ const HANDOFF_SCHEMA_VERSION = '2';
|
|
|
34
36
|
export function sha256OfBody(body) {
|
|
35
37
|
return createHash('sha256').update(body, 'utf8').digest('hex');
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Both ids in a handoff path are caller-supplied path segments, so both are
|
|
41
|
+
* checked at the join. Added 2026-09-14 (repair R1, security audit F2 of
|
|
42
|
+
* `2026-09-14-cli-id-escape-instrumentation`).
|
|
43
|
+
*
|
|
44
|
+
* This function was introduced by `0536d5bd` — the commit that instrumented
|
|
45
|
+
* this defect class — with neither id checked, and it sat outside rule D's
|
|
46
|
+
* scanned layer, so the instrument could not see its own new member.
|
|
47
|
+
* Measured on the pre-fix tree (`prd handoff init --apply`, temp project,
|
|
48
|
+
* `ok: true` both times):
|
|
49
|
+
*
|
|
50
|
+
* --rid '../../../../../../README' replaced the project-root README.md
|
|
51
|
+
* --sid '../../../../SIDOUT' wrote 4 levels above the project root
|
|
52
|
+
*
|
|
53
|
+
* The two axes need two different controls, for a recorded reason: the rid has
|
|
54
|
+
* a pinned format (`REQUEST_ID_PATTERN`, no separator, no dot-dot, no drive)
|
|
55
|
+
* and the sid has none, so it gets the segment check. `isUnsafePathInput`
|
|
56
|
+
* alone is NOT enough for the rid — it admits `a/b` (two non-empty segments),
|
|
57
|
+
* which `request-artifact-service.ts` would reject.
|
|
58
|
+
*
|
|
59
|
+
* Guarding HERE rather than at the three `prd`/`env` flags means every producer
|
|
60
|
+
* that writes through this constructor — `initHandoff`'s default,
|
|
61
|
+
* `handoff-auto-regen.ts`, `evidence-generator.ts` — is covered by the join
|
|
62
|
+
* itself, not by each caller re-deciding.
|
|
63
|
+
*/
|
|
64
|
+
function assertSafeHandoffIds(sessionId, requestId) {
|
|
65
|
+
if (!REQUEST_ID_PATTERN.test(requestId)) {
|
|
66
|
+
throw new Error(`Invalid request id: ${requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
67
|
+
}
|
|
68
|
+
if (isUnsafePathInput(sessionId)) {
|
|
69
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
37
72
|
/**
|
|
38
73
|
* The canonical capsule path for ONE SLICE, relative to the project root.
|
|
39
74
|
*
|
|
@@ -50,6 +85,7 @@ export function sha256OfBody(body) {
|
|
|
50
85
|
* tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
|
|
51
86
|
*/
|
|
52
87
|
export function handoffRelativePath(sessionId, requestId) {
|
|
88
|
+
assertSafeHandoffIds(sessionId, requestId);
|
|
53
89
|
return join('.peaks', '_runtime', sessionId, 'prd', `handoff-${requestId}.md`);
|
|
54
90
|
}
|
|
55
91
|
/**
|
|
@@ -69,6 +105,13 @@ export function handoffRelativePath(sessionId, requestId) {
|
|
|
69
105
|
* this scoping exists to close (fail closed, not "some capsule is there").
|
|
70
106
|
*/
|
|
71
107
|
export function resolveHandoffPath(opts) {
|
|
108
|
+
// The legacy bare-name candidate below is a second join of the same sid, in a
|
|
109
|
+
// second function, so it needs the sid guarded in its own right — the
|
|
110
|
+
// optional-requestId branch reaches `handoffRelativePath` (guarded there), but
|
|
111
|
+
// the branch that is taken when a caller has NO rid reaches this join only.
|
|
112
|
+
if (isUnsafePathInput(opts.sessionId)) {
|
|
113
|
+
throw new Error(`Invalid session id: ${opts.sessionId} (must be a single path segment)`);
|
|
114
|
+
}
|
|
72
115
|
const candidates = [
|
|
73
116
|
...(opts.requestId !== undefined ? [handoffRelativePath(opts.sessionId, opts.requestId)] : []),
|
|
74
117
|
join('.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md')
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
20
|
+
import { REQUEST_ID_PATTERN } from '../artifacts/request-artifact-service.js';
|
|
20
21
|
/** 6-item business checklist (12 Gaps QA perspective). */
|
|
21
22
|
export const QA_BUSINESS_ITEMS = [
|
|
22
23
|
{ id: 'business-flow', question: '这个功能"用起来"对吗?(业务流程顺不顺,操作路径是否反人类,跟现有系统交互有没有断层)' },
|
|
@@ -37,17 +38,30 @@ export function buildEmptyQaReview(requestId, sessionId, now = new Date()) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
export function getQaReviewDir(projectRoot, sessionId) {
|
|
40
|
-
// Sid axis.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
41
|
+
// Sid axis ONLY. The guard here covers the session segment of every
|
|
42
|
+
// `peaks qa-business-review|score|accept|reject` subcommand — measured:
|
|
43
|
+
// `--session-id ../../../../…/PWNED` wrote `qa-business-reviews/<rid>.json`
|
|
44
|
+
// outside every project root under an `ok: true` envelope (RD sweep case A17).
|
|
45
|
+
//
|
|
46
|
+
// It does NOT cover the rid axis: the request id is joined one function later,
|
|
47
|
+
// in `getQaReviewPath` below. Corrected 2026-09-14 (repair R1) — this comment
|
|
48
|
+
// previously said "one guard here covers the whole family", and the security
|
|
49
|
+
// audit of `2026-09-14-cli-id-escape-instrumentation` (F1b) measured that claim
|
|
50
|
+
// false: `qa-business-review '../../../../…/EVILQA3'` wrote a `.json` file
|
|
51
|
+
// outside the project root under `ok: true`.
|
|
45
52
|
if (isUnsafePathInput(sessionId)) {
|
|
46
53
|
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
47
54
|
}
|
|
48
55
|
return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'qa-business-reviews');
|
|
49
56
|
}
|
|
50
57
|
export function getQaReviewPath(projectRoot, sessionId, requestId) {
|
|
58
|
+
// Rid axis. The requestId is the CLI positional (`qa-business-review
|
|
59
|
+
// <request-id>`), so it is caller-supplied and it becomes a filename segment
|
|
60
|
+
// here. `isUnsafePathInput` alone would admit `a/b`; the rid has a pinned
|
|
61
|
+
// format, so the format check is the control.
|
|
62
|
+
if (!REQUEST_ID_PATTERN.test(requestId)) {
|
|
63
|
+
throw new Error(`Invalid request id: ${requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
64
|
+
}
|
|
51
65
|
return join(getQaReviewDir(projectRoot, sessionId), `${requestId}.json`);
|
|
52
66
|
}
|
|
53
67
|
export function readQaReview(projectRoot, sessionId, requestId) {
|
|
@@ -51,6 +51,14 @@ export type CommitBoundary = {
|
|
|
51
51
|
syncState: 'synced' | 'pending' | 'failed';
|
|
52
52
|
rollbackPoint: string | null;
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* The repo's slice-id control — no separator, no drive, no `..`, non-empty, and
|
|
56
|
+
* not a bare `.`/`..`. Exported 2026-09-14 (repair R1) so the slice-id axis has
|
|
57
|
+
* ONE control: `slice-review-state.getReviewPath` joins a caller-supplied slice
|
|
58
|
+
* id into a filename and previously had none, and a second copy of this regex
|
|
59
|
+
* there would be the same axis decided twice.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SLICE_ID_PATTERN: RegExp;
|
|
54
62
|
/**
|
|
55
63
|
* Resolution sources for `resolveArtifactSession`, in priority order.
|
|
56
64
|
* - `active-skill`: the canonical sid-scoped lease projection
|
|
@@ -44,7 +44,14 @@ const MODERN_RETENTION_REQUIREMENTS = [
|
|
|
44
44
|
'qa/test-reports/{sliceId}.md',
|
|
45
45
|
'txt/handoff.md'
|
|
46
46
|
];
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The repo's slice-id control — no separator, no drive, no `..`, non-empty, and
|
|
49
|
+
* not a bare `.`/`..`. Exported 2026-09-14 (repair R1) so the slice-id axis has
|
|
50
|
+
* ONE control: `slice-review-state.getReviewPath` joins a caller-supplied slice
|
|
51
|
+
* id into a filename and previously had none, and a second copy of this regex
|
|
52
|
+
* there would be the same axis decided twice.
|
|
53
|
+
*/
|
|
54
|
+
export const SLICE_ID_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
48
55
|
function getPeaksPath(workspaceRoot) {
|
|
49
56
|
return resolve(workspaceRoot, '.peaks');
|
|
50
57
|
}
|
|
@@ -8,7 +8,25 @@
|
|
|
8
8
|
* is not merely annotated. `confidence: 'exact'` is reserved for lines where
|
|
9
9
|
* both sides were parsed AND the recorded interface was fully readable.
|
|
10
10
|
*/
|
|
11
|
+
import { realpathSync } from 'node:fs';
|
|
11
12
|
import { resolve, sep } from 'node:path';
|
|
13
|
+
/**
|
|
14
|
+
* Canonicalize through symlinks so two paths that name the same directory through
|
|
15
|
+
* different prefixes (the macOS `/var` <-> `/private/var` quirk the brief calls
|
|
16
|
+
* out) line up as the same literal on both sides of the prefix test below.
|
|
17
|
+
* Falls back to `resolve()` when the path does not exist yet — `realpathSync`
|
|
18
|
+
* throws on missing paths, and several callers in the suite inspect not-yet-
|
|
19
|
+
* created paths (e.g. the "string prefix of a sibling" guard at
|
|
20
|
+
* api-diff-service.test.ts:462).
|
|
21
|
+
*/
|
|
22
|
+
function safeRealpath(p) {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(p);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return resolve(p);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
12
30
|
/** The honest boundary of the feature. Printed in the output, not just documented. */
|
|
13
31
|
export const NOT_DETECTABLE = [
|
|
14
32
|
'a field whose type is unchanged but whose meaning changed',
|
|
@@ -38,8 +56,8 @@ export function isRecord(value) {
|
|
|
38
56
|
* the misleading `archive/docs/api.json`.
|
|
39
57
|
*/
|
|
40
58
|
export function toDisplayPath(projectRoot, file) {
|
|
41
|
-
const rootParts =
|
|
42
|
-
const fileParts =
|
|
59
|
+
const rootParts = safeRealpath(projectRoot).split(sep);
|
|
60
|
+
const fileParts = safeRealpath(file).split(sep);
|
|
43
61
|
const inside = fileParts.length > rootParts.length
|
|
44
62
|
&& rootParts.every((part, index) => part === fileParts[index]);
|
|
45
63
|
return (inside ? fileParts.slice(rootParts.length) : fileParts).join('/');
|
|
@@ -71,12 +71,30 @@ export function assertSafeDispatchRecordPath(recordPath, projectRoot) {
|
|
|
71
71
|
// to create it). Fall back to lexical comparison against the
|
|
72
72
|
// canonical projectRoot — the write will then create the file,
|
|
73
73
|
// and any symlink in the parent will be caught on the next read.
|
|
74
|
+
//
|
|
75
|
+
// macOS note: the project's own realpath may still resolve the
|
|
76
|
+
// `/var -> /private/var` symlink even when the sub-agent dir does
|
|
77
|
+
// not exist yet, so the caller can compare on a stable prefix
|
|
78
|
+
// instead of having one side of the comparison on `/var/...` and
|
|
79
|
+
// the other on `/private/var/...`. We still validate that the
|
|
80
|
+
// canonical record path lives under the canonical projectRoot.
|
|
74
81
|
const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
|
|
75
82
|
const rel2 = relative(fallback, recordPath);
|
|
76
83
|
if (rel2.startsWith('..') || isAbsolute(rel2)) {
|
|
77
84
|
throw invalidPathError(recordPath, 'must be under .peaks/_sub_agents/');
|
|
78
85
|
}
|
|
79
|
-
|
|
86
|
+
try {
|
|
87
|
+
realRoot = realpathSync(projectRoot);
|
|
88
|
+
const canonicalRecord = resolve(realRoot, '.peaks', SUB_AGENTS_DIR, recordPath.slice(fallback.length + 1));
|
|
89
|
+
const realRel = relative(realRoot, canonicalRecord);
|
|
90
|
+
if (realRel.startsWith('..' + sep) || realRel === '..' || isAbsolute(realRel)) {
|
|
91
|
+
throw invalidPathError(recordPath, 'escapes project root via symlink');
|
|
92
|
+
}
|
|
93
|
+
return canonicalRecord;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return recordPath;
|
|
97
|
+
}
|
|
80
98
|
}
|
|
81
99
|
const realRel = relative(realRoot, realRecord);
|
|
82
100
|
if (realRel.startsWith('..' + sep) || realRel === '..' || isAbsolute(realRel)) {
|
|
@@ -1 +1,34 @@
|
|
|
1
1
|
export declare function getSessionDir(projectRoot: string, sessionId: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* The TOTAL entry to the same axis. Same predicate, same path; the only
|
|
4
|
+
* difference is that this one is total — it never throws.
|
|
5
|
+
*
|
|
6
|
+
* WHY TWO ENTRIES RATHER THAN ONE. The partial entry above is correct
|
|
7
|
+
* for a caller that cannot proceed without a session dir: a throw is
|
|
8
|
+
* the one failure a caller cannot forget to handle. It is the WRONG
|
|
9
|
+
* shape for a frame whose own doc promises never to throw — the
|
|
10
|
+
* statusline, the fire-and-forget telemetry writer, the best-effort
|
|
11
|
+
* probe. Each of those frames had to wrap the partial entry in a
|
|
12
|
+
* `try { } catch { return null }`, and that swallow cannot be told
|
|
13
|
+
* apart from a real "there is nothing here" answer. Measured on the
|
|
14
|
+
* compact backoff (`auto-compact-lifecycle.ts`), the conflation turned
|
|
15
|
+
* an unresolvable id into the ADMIT branch and re-opened a dispatch
|
|
16
|
+
* that the open run should have suppressed.
|
|
17
|
+
*
|
|
18
|
+
* The split does NOT make a swallow unwriteable — TypeScript has no
|
|
19
|
+
* checked exceptions, so nothing here can. It makes the swallow
|
|
20
|
+
* UNNECESSARY at a named place, and it gives every checker a stable
|
|
21
|
+
* name to key on: a frame that degrades must say so by calling this
|
|
22
|
+
* function, and the degrading branch (`ok: false`) is then a value the
|
|
23
|
+
* caller has to handle rather than a `catch` nobody reads.
|
|
24
|
+
*
|
|
25
|
+
* `reason` is a single-line English sentence fit for an envelope — no
|
|
26
|
+
* stack traces, no CLI verbs (see `human-nl-choice-only-tenet`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function tryGetSessionDir(projectRoot: string, sessionId: string): {
|
|
29
|
+
readonly ok: true;
|
|
30
|
+
readonly dir: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly ok: false;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
};
|
|
@@ -22,11 +22,71 @@
|
|
|
22
22
|
* legacy `.peaks/<sid>/...` artifact path that a sub-agent would
|
|
23
23
|
* follow verbatim.
|
|
24
24
|
*
|
|
25
|
+
* `sessionId` is the last caller-supplied segment of every session-scoped
|
|
26
|
+
* path here, so its segment check lives at this join rather than being
|
|
27
|
+
* re-derived at each call site: `../../x` used to be joined verbatim, and
|
|
28
|
+
* the CLI wrote outside the project root while still returning `ok: true`.
|
|
29
|
+
*
|
|
30
|
+
* The predicate is `isUnsafePathInput`, NOT `SESSION_ID_PATTERN` /
|
|
31
|
+
* `validateSessionId`. The latter are stricter than the segment axis and
|
|
32
|
+
* reject ids that are legal today (`sid-1`, a request id reused as the
|
|
33
|
+
* session-dir name), so adopting them would change results for
|
|
34
|
+
* well-formed callers.
|
|
35
|
+
*
|
|
36
|
+
* Not covered, recorded rather than implied: the `\0` axis
|
|
37
|
+
* (`isUnsafePathInput` admits a NUL, and `execFileSync` raises EINVAL
|
|
38
|
+
* before it can be exercised), and callers that hand-roll
|
|
39
|
+
* `join(root, '.peaks', '_runtime', sid, ...)` instead of calling this.
|
|
40
|
+
*
|
|
25
41
|
* @param projectRoot - Absolute path to the project root.
|
|
26
42
|
* @param sessionId - The session identifier (e.g. `2026-06-06-session-5b1095`).
|
|
27
43
|
* @returns Absolute path to the canonical session directory.
|
|
44
|
+
* @throws Error when `sessionId` is not a single path segment.
|
|
28
45
|
*/
|
|
29
46
|
import { join } from 'node:path';
|
|
47
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
30
48
|
export function getSessionDir(projectRoot, sessionId) {
|
|
49
|
+
// Throwing, not `null` / a tagged result: those widen the return type
|
|
50
|
+
// to `string | null` and make every call site handle a bad id — the
|
|
51
|
+
// per-site slice this guard replaces. This is also the shape the repo
|
|
52
|
+
// already refuses with, and a caller cannot forget to handle a throw.
|
|
53
|
+
if (isUnsafePathInput(sessionId)) {
|
|
54
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
55
|
+
}
|
|
31
56
|
return join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
32
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The TOTAL entry to the same axis. Same predicate, same path; the only
|
|
60
|
+
* difference is that this one is total — it never throws.
|
|
61
|
+
*
|
|
62
|
+
* WHY TWO ENTRIES RATHER THAN ONE. The partial entry above is correct
|
|
63
|
+
* for a caller that cannot proceed without a session dir: a throw is
|
|
64
|
+
* the one failure a caller cannot forget to handle. It is the WRONG
|
|
65
|
+
* shape for a frame whose own doc promises never to throw — the
|
|
66
|
+
* statusline, the fire-and-forget telemetry writer, the best-effort
|
|
67
|
+
* probe. Each of those frames had to wrap the partial entry in a
|
|
68
|
+
* `try { } catch { return null }`, and that swallow cannot be told
|
|
69
|
+
* apart from a real "there is nothing here" answer. Measured on the
|
|
70
|
+
* compact backoff (`auto-compact-lifecycle.ts`), the conflation turned
|
|
71
|
+
* an unresolvable id into the ADMIT branch and re-opened a dispatch
|
|
72
|
+
* that the open run should have suppressed.
|
|
73
|
+
*
|
|
74
|
+
* The split does NOT make a swallow unwriteable — TypeScript has no
|
|
75
|
+
* checked exceptions, so nothing here can. It makes the swallow
|
|
76
|
+
* UNNECESSARY at a named place, and it gives every checker a stable
|
|
77
|
+
* name to key on: a frame that degrades must say so by calling this
|
|
78
|
+
* function, and the degrading branch (`ok: false`) is then a value the
|
|
79
|
+
* caller has to handle rather than a `catch` nobody reads.
|
|
80
|
+
*
|
|
81
|
+
* `reason` is a single-line English sentence fit for an envelope — no
|
|
82
|
+
* stack traces, no CLI verbs (see `human-nl-choice-only-tenet`).
|
|
83
|
+
*/
|
|
84
|
+
export function tryGetSessionDir(projectRoot, sessionId) {
|
|
85
|
+
// Deliberately NOT `try { return {ok:true, dir: getSessionDir(...)} } catch`.
|
|
86
|
+
// That would re-introduce the swallow this function exists to remove, and it
|
|
87
|
+
// would also catch a throw from `join` for reasons that are not a bad id.
|
|
88
|
+
if (isUnsafePathInput(sessionId)) {
|
|
89
|
+
return { ok: false, reason: `Invalid session id: ${sessionId} (must be a single path segment)` };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, dir: join(projectRoot, '.peaks', '_runtime', sessionId) };
|
|
92
|
+
}
|
|
@@ -32,14 +32,14 @@ export declare const SkillSearchInputSchema: z.ZodObject<{
|
|
|
32
32
|
status: "status";
|
|
33
33
|
resume: "resume";
|
|
34
34
|
audit: "audit";
|
|
35
|
-
|
|
35
|
+
"final-review": "final-review";
|
|
36
36
|
content: "content";
|
|
37
|
+
ide: "ide";
|
|
37
38
|
test: "test";
|
|
38
|
-
research: "research";
|
|
39
39
|
doctor: "doctor";
|
|
40
|
+
research: "research";
|
|
40
41
|
triage: "triage";
|
|
41
42
|
sop: "sop";
|
|
42
|
-
"final-review": "final-review";
|
|
43
43
|
"slice-decompose": "slice-decompose";
|
|
44
44
|
"issue-fix-orchestrator": "issue-fix-orchestrator";
|
|
45
45
|
"perf-audit": "perf-audit";
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
18
|
+
import { SLICE_ID_PATTERN } from '../sc/sc-service.js';
|
|
18
19
|
/** The 5 default review items per slice (the 12 Gaps memory checklist). */
|
|
19
20
|
export const DEFAULT_REVIEW_ITEMS = [
|
|
20
21
|
{ id: 'business-match', question: '这个 slice 做完,业务流程对吗?(跟产品最初给的需求匹配)' },
|
|
@@ -33,16 +34,30 @@ export function buildEmptySliceReview(sliceId, sessionId, now = new Date()) {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
export function getReviewDir(projectRoot, sessionId) {
|
|
36
|
-
// Sid axis. Every `peaks slice-review|score|accept|reject` subcommand
|
|
37
|
-
// the runtime tree through this one constructor. Measured:
|
|
38
|
-
// ../../../../…/PWNED` wrote `slice-reviews/<slice-id>.json`
|
|
39
|
-
// project root under an `ok: true` envelope (RD sweep case A26).
|
|
37
|
+
// Sid axis ONLY. Every `peaks slice-review|score|accept|reject` subcommand
|
|
38
|
+
// reaches the runtime tree through this one constructor. Measured:
|
|
39
|
+
// `--session-id ../../../../…/PWNED` wrote `slice-reviews/<slice-id>.json`
|
|
40
|
+
// outside every project root under an `ok: true` envelope (RD sweep case A26).
|
|
41
|
+
//
|
|
42
|
+
// It does NOT cover the slice-id axis: the slice id is joined one function
|
|
43
|
+
// later, in `getReviewPath` below. Corrected 2026-09-14 (repair R1) — this
|
|
44
|
+
// comment previously said one guard covered the whole family; the security
|
|
45
|
+
// audit of `2026-09-14-cli-id-escape-instrumentation` (F1b) measured that
|
|
46
|
+
// false: `slice-review '../../../../…/EVILSL'` wrote a `.json` file outside
|
|
47
|
+
// the project root under `ok: true`.
|
|
40
48
|
if (isUnsafePathInput(sessionId)) {
|
|
41
49
|
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
42
50
|
}
|
|
43
51
|
return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'slice-reviews');
|
|
44
52
|
}
|
|
45
53
|
export function getReviewPath(projectRoot, sessionId, sliceId) {
|
|
54
|
+
// Slice-id axis. The slice id is the CLI positional (`slice-review
|
|
55
|
+
// <slice-id>`), so it is caller-supplied and it becomes a filename segment
|
|
56
|
+
// here. Same control as the rid axis: a pinned format beats the segment check,
|
|
57
|
+
// which admits `a/b`.
|
|
58
|
+
if (!SLICE_ID_PATTERN.test(sliceId)) {
|
|
59
|
+
throw new Error(`Invalid slice id: ${sliceId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
60
|
+
}
|
|
46
61
|
return join(getReviewDir(projectRoot, sessionId), `${sliceId}.json`);
|
|
47
62
|
}
|
|
48
63
|
export function readSliceReview(projectRoot, sessionId, sliceId) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `peaks standards lint --category loop-engineering` (registered in M0's
|
|
19
19
|
* plan) will read this file from disk and call this function.
|
|
20
20
|
*/
|
|
21
|
-
export declare const EXPECTED_RED_LINE_IDS: readonly ["RL-0", "RL-1", "RL-2", "RL-3", "RL-4", "RL-5", "RL-6", "RL-7", "RL-8", "RL-9"];
|
|
21
|
+
export declare const EXPECTED_RED_LINE_IDS: readonly ["RL-0", "RL-1", "RL-2", "RL-3", "RL-4", "RL-5", "RL-6", "RL-7", "RL-8", "RL-9", "RL-10"];
|
|
22
22
|
export type RedLineId = (typeof EXPECTED_RED_LINE_IDS)[number];
|
|
23
23
|
export declare const REQUIRED_SECTIONS: readonly ["Failure modes", "Rewrite", "Self-check", "Out-of-scope"];
|
|
24
24
|
export type RedLineSection = (typeof REQUIRED_SECTIONS)[number];
|
|
@@ -29,6 +29,12 @@ export const EXPECTED_RED_LINE_IDS = [
|
|
|
29
29
|
'RL-7',
|
|
30
30
|
'RL-8',
|
|
31
31
|
'RL-9',
|
|
32
|
+
// RL-10 was written into the guideline file (its 4 sections included) but
|
|
33
|
+
// never added here, so the lint could not see it: the closed set stopped one
|
|
34
|
+
// short of the file it was supposed to police. The file's own footer said
|
|
35
|
+
// "Total red lines: 9 (RL-0..RL-9)" while RL-10 sat below it — the two
|
|
36
|
+
// statements contradicted each other and only the file was ever read.
|
|
37
|
+
'RL-10',
|
|
32
38
|
];
|
|
33
39
|
export const REQUIRED_SECTIONS = [
|
|
34
40
|
'Failure modes',
|
|
@@ -5,10 +5,29 @@
|
|
|
5
5
|
* WRITE goes through `assertUnder` first — the slice-wide guard against an
|
|
6
6
|
* artifact escaping `<root>/.peaks/_runtime/<sid>/web/` (AC1).
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname } from 'node:path';
|
|
10
10
|
import { assertUnder, webDaemonDir, webDaemonInfoPath, webSpawnLockPath } from './web-artifact-paths.js';
|
|
11
11
|
import { parseDaemonInfo } from './web-protocol.js';
|
|
12
|
+
/**
|
|
13
|
+
* Canonicalize through symlinks so two paths that name the same directory through
|
|
14
|
+
* different prefixes compare equal — the macOS `/var` <-> `/private/var` quirk
|
|
15
|
+
* makes `mkdtempSync(...)` return `/var/folders/...` while `process.cwd()` after
|
|
16
|
+
* `chdir` returns `/private/var/folders/...`. They are the same directory on
|
|
17
|
+
* disk; without `realpathSync` the ownership check below silently rejects a
|
|
18
|
+
* legitimate record and the CLI cold-starts a real daemon instead of reusing
|
|
19
|
+
* the stub. Falls back to `resolve()` when the path is absent: a planted
|
|
20
|
+
* record naming a directory that does not exist still has to fail the
|
|
21
|
+
* containment test, and a `realpathSync` on it would throw.
|
|
22
|
+
*/
|
|
23
|
+
function safeRealpath(p) {
|
|
24
|
+
try {
|
|
25
|
+
return realpathSync(p);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
/** A spawn lock older than this is reclaimed even if its owner pid is alive. */
|
|
13
32
|
const SPAWN_LOCK_STALE_MS = 120_000;
|
|
14
33
|
/**
|
|
@@ -46,7 +65,13 @@ export function readDaemonInfo(projectRoot, sessionId) {
|
|
|
46
65
|
}
|
|
47
66
|
try {
|
|
48
67
|
const info = parseDaemonInfo(readFileSync(target, 'utf8'));
|
|
49
|
-
if (info === null || info.
|
|
68
|
+
if (info === null || info.sessionId !== sessionId) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Canonicalize both sides — a record written from one prefix (say
|
|
72
|
+
// `/var/folders/...`) and read from the canonical one (`/private/var/...`)
|
|
73
|
+
// describes the same project on macOS.
|
|
74
|
+
if (safeRealpath(info.projectRoot) !== safeRealpath(projectRoot)) {
|
|
50
75
|
return null;
|
|
51
76
|
}
|
|
52
77
|
return info;
|
|
@@ -28,17 +28,16 @@ export async function findRequestFile(projectRoot, role, rid) {
|
|
|
28
28
|
const artifact = await showRequestArtifact({ projectRoot, role: role, requestId: rid });
|
|
29
29
|
if (artifact === null)
|
|
30
30
|
return null;
|
|
31
|
-
// Slice 2026-06-28-code-mode-bypass-fix (defect #3)
|
|
32
|
-
// `showRequestArtifact`
|
|
33
|
-
// as `sessionId
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return { path: artifact.path, content: artifact.content, sessionId };
|
|
31
|
+
// Slice 2026-06-28-code-mode-bypass-fix (defect #3) used to strip a
|
|
32
|
+
// `_runtime/` prefix here, because `showRequestArtifact` then returned the
|
|
33
|
+
// FULL SCOPE (`_runtime/<sid>`) as `sessionId`. Repair R5 removed that
|
|
34
|
+
// round-trip at its source: `readSummary` now builds the summary from the
|
|
35
|
+
// already-resolved directory and the bare id, so `sessionId` is the bare id
|
|
36
|
+
// and the prefix strip could never fire. It was removed by repair R7 rather
|
|
37
|
+
// than left in place as dead code with a comment asserting a behaviour its
|
|
38
|
+
// callee no longer has — an artifact claiming something the code does not do
|
|
39
|
+
// is the defect class this line of work exists to remove.
|
|
40
|
+
return { path: artifact.path, content: artifact.content, sessionId: artifact.sessionId };
|
|
42
41
|
}
|
|
43
42
|
/**
|
|
44
43
|
* Where the CURRENT contract puts one gate's evidence.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* returns a structured `PipelineVerification` envelope. Type
|
|
6
6
|
* declarations live in `pipeline-verify-types.ts`; private gate
|
|
7
7
|
* helpers (`rdGatesForType`, `qaGatesForType`, `extractState`,
|
|
8
|
-
* `findRequestFile`, the
|
|
8
|
+
* `findRequestFile`, the RD/QA handoff
|
|
9
9
|
* state sets, and the RD/QA evidence path probes) live in
|
|
10
10
|
* `pipeline-verify-gate-support.ts`. The re-export shim at the
|
|
11
11
|
* bottom preserves the original public surface so existing import
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* returns a structured `PipelineVerification` envelope. Type
|
|
6
6
|
* declarations live in `pipeline-verify-types.ts`; private gate
|
|
7
7
|
* helpers (`rdGatesForType`, `qaGatesForType`, `extractState`,
|
|
8
|
-
* `findRequestFile`, the
|
|
8
|
+
* `findRequestFile`, the RD/QA handoff
|
|
9
9
|
* state sets, and the RD/QA evidence path probes) live in
|
|
10
10
|
* `pipeline-verify-gate-support.ts`. The re-export shim at the
|
|
11
11
|
* bottom preserves the original public surface so existing import
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { isRequestType } from '../artifacts/artifact-prerequisites.js';
|
|
17
17
|
import { readSkipState } from './workflow-state-store.js';
|
|
18
18
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
19
|
-
import { listUnpromotedFeedback } from '../feedback/feedback-promotion-service.js';
|
|
19
|
+
import { listPromotionExempt, listUnpromotedFeedback } from '../feedback/feedback-promotion-service.js';
|
|
20
20
|
import { QA_COMPLETE_STATES, RD_QA_HANDOFF_STATES, extractState, findRequestFile, qaGatesForType, rdGatesForType, resolveQaEvidencePaths, resolveRdEvidencePaths } from './pipeline-verify-gate-support.js';
|
|
21
21
|
export async function verifyPipeline(options) {
|
|
22
22
|
const requestType = isRequestType(options.requestType ?? '') ? options.requestType : 'feature';
|
|
@@ -178,10 +178,12 @@ export async function verifyPipeline(options) {
|
|
|
178
178
|
}
|
|
179
179
|
// Slice 002 (v2.15.0) AC-3 — Gate H "feedback-promotion". Scans
|
|
180
180
|
// `.peaks/memory/*.md` for `metadata.type === 'feedback'` entries
|
|
181
|
-
//
|
|
182
|
-
// sidecar)
|
|
183
|
-
//
|
|
184
|
-
//
|
|
181
|
+
// that lack a promotion marker (HTML comment or `.promotion.json`
|
|
182
|
+
// sidecar) OR whose marker is not backed by the layer's artifact
|
|
183
|
+
// (rid 2026-09-14-gate-h-promotion). When any such feedback is
|
|
184
|
+
// found, the gate fails and the pipeline does not complete until the
|
|
185
|
+
// user promotes via `peaks feedback promote <memory-file> --layer
|
|
186
|
+
// <A|B|C>` — which now produces the artifact, not just the marker.
|
|
185
187
|
//
|
|
186
188
|
// The scan is intentionally non-throwing — a missing or unreadable
|
|
187
189
|
// memory dir is treated as "no feedback found, gate passes" so
|
|
@@ -196,14 +198,25 @@ export async function verifyPipeline(options) {
|
|
|
196
198
|
];
|
|
197
199
|
try {
|
|
198
200
|
const unpromoted = listUnpromotedFeedback({ projectRoot: options.projectRoot });
|
|
201
|
+
// rid 2026-09-14-gate-h-promotion (classify slice): memories that declare
|
|
202
|
+
// themselves out of the gate are reported, never dropped. An exemption the
|
|
203
|
+
// gate does not show would be indistinguishable from a fixed violation.
|
|
204
|
+
const exempt = listPromotionExempt({ projectRoot: options.projectRoot });
|
|
205
|
+
const exemptNote = exempt.length === 0
|
|
206
|
+
? ''
|
|
207
|
+
: `; ${exempt.length} declared not-to-be-promoted: ${exempt.map((e) => `${e.name} (${e.code})`).join('; ')}`;
|
|
199
208
|
if (unpromoted.length === 0) {
|
|
200
209
|
feedbackGates[0].passed = true;
|
|
201
|
-
feedbackGates[0].detail = `0 unpromoted feedback memories in .peaks/memory
|
|
210
|
+
feedbackGates[0].detail = `0 unpromoted feedback memories in .peaks/memory/${exemptNote}`;
|
|
202
211
|
}
|
|
203
212
|
else {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
// rid 2026-09-14-gate-h-promotion: the gate no longer passes on a marker
|
|
214
|
+
// alone. `listUnpromotedFeedback` now also reports markers whose layer
|
|
215
|
+
// artifact is absent, so `unpromoted` mixes "never promoted" with
|
|
216
|
+
// "promoted on paper only" — the reason on each entry says which.
|
|
217
|
+
feedbackGates[0].detail = `${unpromoted.length} feedback memor${unpromoted.length === 1 ? 'y' : 'ies'} without a backed promotion: ${unpromoted.map((u) => `${u.name} (${u.reason})`).join('; ')}${exemptNote}`;
|
|
218
|
+
violations.push(`Gate H feedback-promotion FAILED: ${unpromoted.length} feedback memor${unpromoted.length === 1 ? 'y is' : 'ies are'} not yet promoted to an enforcement layer with a real artifact (${unpromoted.map((u) => u.name).join(', ')})${exemptNote}. A marker alone does not count: layer A needs a registered SOP manifest, layer B a hook command that runs something named after the rule in .peaks/.claude-settings-template.json, layer C a hard-floor category in src/services/code/mode-gate.ts. Run \`peaks feedback promote <memory-file> --layer <A|B|C>\` for each and read what it reports. See sops/feedback-promotion-sop.md.`);
|
|
219
|
+
nextActions.push(`Run \`peaks feedback promote <memory-file> --layer <A|B|C>\` for each feedback memory without a backed promotion to satisfy Gate H.`);
|
|
207
220
|
}
|
|
208
221
|
}
|
|
209
222
|
catch {
|
|
@@ -50,9 +50,11 @@ export type PipelineVerification = {
|
|
|
50
50
|
* Slice 002 (v2.15.0) AC-3: Gate H "feedback-promotion". Always
|
|
51
51
|
* present (single-element array). Evaluates whether every
|
|
52
52
|
* `metadata.type === 'feedback'` memory in `.peaks/memory/`
|
|
53
|
-
* carries a promotion marker (comment OR sidecar)
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* carries a promotion marker (comment OR sidecar) AND whether the
|
|
54
|
+
* layer that marker claims is actually backed by its artifact
|
|
55
|
+
* (rid 2026-09-14-gate-h-promotion). Failures block the `complete`
|
|
56
|
+
* verdict via the `gateH` field below; the pipeline only completes
|
|
57
|
+
* when every gate in this array passes.
|
|
56
58
|
*/
|
|
57
59
|
feedbackPhase?: {
|
|
58
60
|
gates: PipelineGate[];
|