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
|
@@ -8,23 +8,27 @@
|
|
|
8
8
|
* primitive behind the `peaks feedback promote` and
|
|
9
9
|
* `peaks feedback check-unpromoted` CLI commands.
|
|
10
10
|
*
|
|
11
|
-
* Promotion tracking convention
|
|
12
|
-
* "promoted" when one of the following is true:
|
|
11
|
+
* Promotion tracking convention — two parts, and BOTH are required:
|
|
13
12
|
*
|
|
14
|
-
* (a)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* (a) A MARKER, either an HTML comment near the top of the body
|
|
14
|
+
* (`<!-- peaks-feedback-promoted: layer=<A|B|C> -->`) or a sibling
|
|
15
|
+
* `.peaks/memory/<name>.promotion.json` sidecar with
|
|
16
|
+
* `{ layer: "A" | "B" | "C", ... }`. Written by `peaks feedback
|
|
17
|
+
* promote` so a single read of the memory file is enough to see the
|
|
18
|
+
* claimed layer.
|
|
18
19
|
*
|
|
19
|
-
* (b)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* (b) The ARTIFACT that layer implies — see `promotionArtifactChecks`.
|
|
21
|
+
* rid 2026-09-14-gate-h-promotion: the marker alone used to count,
|
|
22
|
+
* which made the gate self-certifying, because the only thing a marker
|
|
23
|
+
* proves is that `peaks feedback promote` ran. Every layer-A marker in
|
|
24
|
+
* this repo pointed at `sops/<name>.md`, a file that did not exist and
|
|
25
|
+
* that no engine reads.
|
|
23
26
|
*
|
|
24
|
-
* The comment marker is the SOURCE OF TRUTH for human review; the
|
|
25
|
-
*
|
|
26
|
-
* to mark a feedback memory as promoted.
|
|
27
|
+
* The comment marker is the SOURCE OF TRUTH for human review; the sidecar is
|
|
28
|
+
* the source of truth for the scanner. Neither is evidence on its own.
|
|
27
29
|
*/
|
|
30
|
+
import { type PromotionArtifactCheck, type PromotionEvidence } from './promotion-artifact-evidence.js';
|
|
31
|
+
export type { PromotionArtifactCheck, PromotionEvidence };
|
|
28
32
|
export type PromotionLayer = 'A' | 'B' | 'C';
|
|
29
33
|
export declare const PROMOTION_LAYERS: readonly PromotionLayer[];
|
|
30
34
|
export type PromotionLayerDetail = {
|
|
@@ -34,6 +38,59 @@ export type PromotionLayerDetail = {
|
|
|
34
38
|
description: string;
|
|
35
39
|
};
|
|
36
40
|
export declare const PROMOTION_LAYER_DETAILS: readonly PromotionLayerDetail[];
|
|
41
|
+
/**
|
|
42
|
+
* rid 2026-09-14-gate-h-promotion — what actually backs a promotion.
|
|
43
|
+
*
|
|
44
|
+
* Before this, a promotion was honored on the marker alone (HTML comment or
|
|
45
|
+
* sidecar). Both are written by `peaks feedback promote` and neither proves
|
|
46
|
+
* that anything was enforced: every layer-A promotion in this repo pointed at
|
|
47
|
+
* `sops/<name>.md`, a file that did not exist and that no engine reads. The
|
|
48
|
+
* gate was therefore self-certifying — it read only what the command it tells
|
|
49
|
+
* you to run had written.
|
|
50
|
+
*
|
|
51
|
+
* A promotion is now honored only when its layer's enforcement surface carries
|
|
52
|
+
* the artifact. The three layers keep artifacts in three different shapes, so
|
|
53
|
+
* the check is a small table rather than one rule:
|
|
54
|
+
*
|
|
55
|
+
* - A (peaks-sop gate): a SOP manifest at `.peaks/sops/<id>/sop.json` AND an
|
|
56
|
+
* entry for `<id>` in `.peaks/sops/registry.json`. The registry half is not
|
|
57
|
+
* decoration: `gate-enforce-service.enforceBashCommand` enumerates SOPs via
|
|
58
|
+
* `readRegistry()`, so an unregistered manifest is off the enforcement path
|
|
59
|
+
* no matter how valid it is.
|
|
60
|
+
* - B (peaks-hooks PreToolUse): `.peaks/.claude-settings-template.json` must
|
|
61
|
+
* register the rule inside its `hooks` block. The file always exists, so
|
|
62
|
+
* existence proves nothing — the evidence is the registration inside it.
|
|
63
|
+
* - C (mode-gate hardFloorCategory): `src/services/code/mode-gate.ts` must
|
|
64
|
+
* register the rule in the hard-floor vocabulary, for the same reason. This
|
|
65
|
+
* is the repo's existing convention: the one real layer-C promotion cites
|
|
66
|
+
* its memory by path, from the category's doc block.
|
|
67
|
+
*
|
|
68
|
+
* R2 (2026-09-14-gate-h-promotion): each of those is now a PARSE plus a shape
|
|
69
|
+
* assertion, in `promotion-artifact-evidence.ts`. Every check used to be a
|
|
70
|
+
* `text.includes(<rule>)` over the whole file, which certified a tree that was
|
|
71
|
+
* a refusal — an invalid-JSON registry, a template saying "do NOT add a
|
|
72
|
+
* matcher", a mode-gate line saying the rule is deliberately not a category —
|
|
73
|
+
* because in each case the rule's name was still in the file's bytes. A name in
|
|
74
|
+
* a file is not a registration, and a file that cannot be parsed is a finding,
|
|
75
|
+
* not a permit.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* SOP id used for a feedback memory's layer-A artifact.
|
|
79
|
+
*
|
|
80
|
+
* Prefixed because `peaks-*` SOP ids are reserved for the built-in namespace
|
|
81
|
+
* (`reservedIdReason` in sop-service.ts) and several feedback memories start
|
|
82
|
+
* with `peaks-`, which would make them unregistrable under their own name.
|
|
83
|
+
*/
|
|
84
|
+
export declare function sopIdForFeedback(memoryName: string): string;
|
|
85
|
+
/** The artifact(s) and the structural evidence each must carry for `layer` to mean anything. */
|
|
86
|
+
export declare function promotionArtifactChecks(memoryName: string, layer: PromotionLayer): PromotionArtifactCheck[];
|
|
87
|
+
/**
|
|
88
|
+
* Which of `checks` are not satisfied under `projectRoot`. Empty means the
|
|
89
|
+
* promotion is backed by its artifact. Never throws, and never permits: a file
|
|
90
|
+
* that is absent, unreadable, or unparseable is a finding, not a warning —
|
|
91
|
+
* "cannot read the evidence" must not read as "the evidence is good".
|
|
92
|
+
*/
|
|
93
|
+
export declare function missingArtifacts(checks: readonly PromotionArtifactCheck[], projectRoot: string): string[];
|
|
37
94
|
export type FeedbackMemory = {
|
|
38
95
|
/** File basename (without `.md`). */
|
|
39
96
|
name: string;
|
|
@@ -59,6 +116,43 @@ export type UnpromotedFeedbackEntry = {
|
|
|
59
116
|
path: string;
|
|
60
117
|
reason: string;
|
|
61
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* rid 2026-09-14-gate-h-promotion (classify slice) — the "not to be promoted"
|
|
121
|
+
* declaration.
|
|
122
|
+
*
|
|
123
|
+
* The gate used to know only `has artifact` / `has no artifact`, so a memory that
|
|
124
|
+
* prescribes no action could never pass: promoting it registers a SOP whose only
|
|
125
|
+
* gate is "the source file still exists", which asserts nothing about behaviour.
|
|
126
|
+
* That is a permanent false positive — the old vacuity defect facing the other way.
|
|
127
|
+
*
|
|
128
|
+
* The declaration closes it, but it must not become a way to silence the gate.
|
|
129
|
+
* It differs from the refused grandfather channel (`promotedAt` older than this
|
|
130
|
+
* rule) in that a grandfather exemption is a property of a memory's AGE: every
|
|
131
|
+
* legacy memory has it, it says nothing about content, and nobody has to assert
|
|
132
|
+
* or defend it. This is a bounded claim about the memory's CONTENT:
|
|
133
|
+
*
|
|
134
|
+
* 1. The code is drawn from a closed vocabulary — free text cannot be used.
|
|
135
|
+
* 2. Each code binds to a predicate over the memory's own frontmatter, which
|
|
136
|
+
* the gate recomputes. The declaration may only RESTATE what the memory
|
|
137
|
+
* already says; it cannot introduce a new fact.
|
|
138
|
+
* 3. A reason string is required (the `closedAt` escape hatch beside it has none).
|
|
139
|
+
* 4. Coexisting with a promotion marker is a contradiction and fails, so the
|
|
140
|
+
* channel cannot be used to bury a promotion whose artifact is missing.
|
|
141
|
+
* 5. Exempted memories stay REPORTED via `listPromotionExempt`, so the
|
|
142
|
+
* unpromoted count never drops silently.
|
|
143
|
+
*/
|
|
144
|
+
export declare const NOT_TO_PROMOTE_CODES: readonly ["non-actionable", "closed-slice-note"];
|
|
145
|
+
export type NotToPromoteCode = (typeof NOT_TO_PROMOTE_CODES)[number];
|
|
146
|
+
export type NotToPromoteRead = {
|
|
147
|
+
kind: 'none';
|
|
148
|
+
} | {
|
|
149
|
+
kind: 'valid';
|
|
150
|
+
code: NotToPromoteCode;
|
|
151
|
+
reason: string;
|
|
152
|
+
} | {
|
|
153
|
+
kind: 'invalid';
|
|
154
|
+
reason: string;
|
|
155
|
+
};
|
|
62
156
|
/**
|
|
63
157
|
* Parse a single `.peaks/memory/<file>.md` into a FeedbackMemory, or
|
|
64
158
|
* `null` when the file is missing / unreadable / not a feedback memory.
|
|
@@ -78,6 +172,26 @@ export declare function parseFeedbackMemory(filePath: string): FeedbackMemory |
|
|
|
78
172
|
export declare function listUnpromotedFeedback(opts: {
|
|
79
173
|
projectRoot: string;
|
|
80
174
|
}): UnpromotedFeedbackEntry[];
|
|
175
|
+
/**
|
|
176
|
+
* Read the memory's not-to-promote declaration. `invalid` is returned rather than
|
|
177
|
+
* `none` when the fields are present but unusable, so the gate fails with a reason
|
|
178
|
+
* instead of quietly treating a malformed declaration as "no declaration".
|
|
179
|
+
*/
|
|
180
|
+
export declare function readNotToPromote(filePath: string): NotToPromoteRead;
|
|
181
|
+
export type PromotionExemptEntry = {
|
|
182
|
+
name: string;
|
|
183
|
+
path: string;
|
|
184
|
+
code: NotToPromoteCode;
|
|
185
|
+
reason: string;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* The feedback memories that declare themselves out of the gate. Exposed so the
|
|
189
|
+
* gate can REPORT them: an exemption nobody can see is the vacuity this whole
|
|
190
|
+
* channel is required to avoid. Never throws.
|
|
191
|
+
*/
|
|
192
|
+
export declare function listPromotionExempt(opts: {
|
|
193
|
+
projectRoot: string;
|
|
194
|
+
}): PromotionExemptEntry[];
|
|
81
195
|
/**
|
|
82
196
|
* Generate the code stub for a given layer. Returns a Markdown
|
|
83
197
|
* snippet the LLM / human can paste into the appropriate file. Pure
|
|
@@ -97,7 +211,16 @@ export type FeedbackPromoteEnvelope = {
|
|
|
97
211
|
feedbackPath: string;
|
|
98
212
|
layer: PromotionLayer;
|
|
99
213
|
layerDetail: string;
|
|
214
|
+
/**
|
|
215
|
+
* Files this call actually wrote. rid 2026-09-14-gate-h-promotion: this used
|
|
216
|
+
* to be the stub's *targets* — paths the command never wrote yet printed as
|
|
217
|
+
* `Generated files:` — which is the defect the Gate H rework exists to remove.
|
|
218
|
+
*/
|
|
100
219
|
generatedFiles: string[];
|
|
220
|
+
/** The artifact(s) `layer` requires before the promotion means anything. */
|
|
221
|
+
requiredArtifacts: string[];
|
|
222
|
+
/** `false` when `requiredArtifacts` are not all present — the marker is then a claim without evidence. */
|
|
223
|
+
effective: boolean;
|
|
101
224
|
snippet: string;
|
|
102
225
|
promotedAt: string;
|
|
103
226
|
promotedBy: string;
|
|
@@ -119,5 +242,5 @@ export declare function promoteFeedback(opts: {
|
|
|
119
242
|
sessionId: string;
|
|
120
243
|
projectRoot: string;
|
|
121
244
|
dryRun?: boolean;
|
|
122
|
-
}): FeedbackPromoteEnvelope
|
|
245
|
+
}): Promise<FeedbackPromoteEnvelope>;
|
|
123
246
|
export declare function isPromotionLayer(value: string): value is PromotionLayer;
|
|
@@ -8,25 +8,30 @@
|
|
|
8
8
|
* primitive behind the `peaks feedback promote` and
|
|
9
9
|
* `peaks feedback check-unpromoted` CLI commands.
|
|
10
10
|
*
|
|
11
|
-
* Promotion tracking convention
|
|
12
|
-
* "promoted" when one of the following is true:
|
|
11
|
+
* Promotion tracking convention — two parts, and BOTH are required:
|
|
13
12
|
*
|
|
14
|
-
* (a)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* (a) A MARKER, either an HTML comment near the top of the body
|
|
14
|
+
* (`<!-- peaks-feedback-promoted: layer=<A|B|C> -->`) or a sibling
|
|
15
|
+
* `.peaks/memory/<name>.promotion.json` sidecar with
|
|
16
|
+
* `{ layer: "A" | "B" | "C", ... }`. Written by `peaks feedback
|
|
17
|
+
* promote` so a single read of the memory file is enough to see the
|
|
18
|
+
* claimed layer.
|
|
18
19
|
*
|
|
19
|
-
* (b)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* (b) The ARTIFACT that layer implies — see `promotionArtifactChecks`.
|
|
21
|
+
* rid 2026-09-14-gate-h-promotion: the marker alone used to count,
|
|
22
|
+
* which made the gate self-certifying, because the only thing a marker
|
|
23
|
+
* proves is that `peaks feedback promote` ran. Every layer-A marker in
|
|
24
|
+
* this repo pointed at `sops/<name>.md`, a file that did not exist and
|
|
25
|
+
* that no engine reads.
|
|
23
26
|
*
|
|
24
|
-
* The comment marker is the SOURCE OF TRUTH for human review; the
|
|
25
|
-
*
|
|
26
|
-
* to mark a feedback memory as promoted.
|
|
27
|
+
* The comment marker is the SOURCE OF TRUTH for human review; the sidecar is
|
|
28
|
+
* the source of truth for the scanner. Neither is evidence on its own.
|
|
27
29
|
*/
|
|
28
30
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
29
31
|
import { dirname, join, resolve } from 'node:path';
|
|
32
|
+
import { registerSop } from '../sop/sop-registry-service.js';
|
|
33
|
+
import { projectRegistryPath, projectSopManifestPath } from '../sop/sop-paths.js';
|
|
34
|
+
import { artifactEvidenceFailure } from './promotion-artifact-evidence.js';
|
|
30
35
|
/**
|
|
31
36
|
* PRD-002b slice 2 — extract magic numbers used by the promotion
|
|
32
37
|
* helper. `slice(0, 5)` was used as a heuristic on the user's body
|
|
@@ -41,7 +46,131 @@ export const PROMOTION_LAYER_DETAILS = [
|
|
|
41
46
|
{ layer: 'B', label: 'peaks-hooks PreToolUse', description: 'Append a matcher to .peaks/.claude-settings-template.json. Tool-call interception.' },
|
|
42
47
|
{ layer: 'C', label: 'mode-gate hardFloorCategory', description: 'Extend HardFloorCategory + shouldPauseAtGate. Always pauses regardless of mode.' }
|
|
43
48
|
];
|
|
49
|
+
/**
|
|
50
|
+
* rid 2026-09-14-gate-h-promotion — what actually backs a promotion.
|
|
51
|
+
*
|
|
52
|
+
* Before this, a promotion was honored on the marker alone (HTML comment or
|
|
53
|
+
* sidecar). Both are written by `peaks feedback promote` and neither proves
|
|
54
|
+
* that anything was enforced: every layer-A promotion in this repo pointed at
|
|
55
|
+
* `sops/<name>.md`, a file that did not exist and that no engine reads. The
|
|
56
|
+
* gate was therefore self-certifying — it read only what the command it tells
|
|
57
|
+
* you to run had written.
|
|
58
|
+
*
|
|
59
|
+
* A promotion is now honored only when its layer's enforcement surface carries
|
|
60
|
+
* the artifact. The three layers keep artifacts in three different shapes, so
|
|
61
|
+
* the check is a small table rather than one rule:
|
|
62
|
+
*
|
|
63
|
+
* - A (peaks-sop gate): a SOP manifest at `.peaks/sops/<id>/sop.json` AND an
|
|
64
|
+
* entry for `<id>` in `.peaks/sops/registry.json`. The registry half is not
|
|
65
|
+
* decoration: `gate-enforce-service.enforceBashCommand` enumerates SOPs via
|
|
66
|
+
* `readRegistry()`, so an unregistered manifest is off the enforcement path
|
|
67
|
+
* no matter how valid it is.
|
|
68
|
+
* - B (peaks-hooks PreToolUse): `.peaks/.claude-settings-template.json` must
|
|
69
|
+
* register the rule inside its `hooks` block. The file always exists, so
|
|
70
|
+
* existence proves nothing — the evidence is the registration inside it.
|
|
71
|
+
* - C (mode-gate hardFloorCategory): `src/services/code/mode-gate.ts` must
|
|
72
|
+
* register the rule in the hard-floor vocabulary, for the same reason. This
|
|
73
|
+
* is the repo's existing convention: the one real layer-C promotion cites
|
|
74
|
+
* its memory by path, from the category's doc block.
|
|
75
|
+
*
|
|
76
|
+
* R2 (2026-09-14-gate-h-promotion): each of those is now a PARSE plus a shape
|
|
77
|
+
* assertion, in `promotion-artifact-evidence.ts`. Every check used to be a
|
|
78
|
+
* `text.includes(<rule>)` over the whole file, which certified a tree that was
|
|
79
|
+
* a refusal — an invalid-JSON registry, a template saying "do NOT add a
|
|
80
|
+
* matcher", a mode-gate line saying the rule is deliberately not a category —
|
|
81
|
+
* because in each case the rule's name was still in the file's bytes. A name in
|
|
82
|
+
* a file is not a registration, and a file that cannot be parsed is a finding,
|
|
83
|
+
* not a permit.
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* SOP id used for a feedback memory's layer-A artifact.
|
|
87
|
+
*
|
|
88
|
+
* Prefixed because `peaks-*` SOP ids are reserved for the built-in namespace
|
|
89
|
+
* (`reservedIdReason` in sop-service.ts) and several feedback memories start
|
|
90
|
+
* with `peaks-`, which would make them unregistrable under their own name.
|
|
91
|
+
*/
|
|
92
|
+
export function sopIdForFeedback(memoryName) {
|
|
93
|
+
return `feedback-${memoryName}`;
|
|
94
|
+
}
|
|
95
|
+
/** The artifact(s) and the structural evidence each must carry for `layer` to mean anything. */
|
|
96
|
+
export function promotionArtifactChecks(memoryName, layer) {
|
|
97
|
+
if (layer === 'A') {
|
|
98
|
+
const id = sopIdForFeedback(memoryName);
|
|
99
|
+
return [
|
|
100
|
+
{ path: `.peaks/sops/${id}/sop.json`, evidence: 'sop-manifest', id },
|
|
101
|
+
{ path: '.peaks/sops/registry.json', evidence: 'sop-registry-entry', id }
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
if (layer === 'B') {
|
|
105
|
+
return [{ path: '.peaks/.claude-settings-template.json', evidence: 'hook-registration', id: memoryName }];
|
|
106
|
+
}
|
|
107
|
+
return [{ path: 'src/services/code/mode-gate.ts', evidence: 'hard-floor-category', id: memoryName }];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Which of `checks` are not satisfied under `projectRoot`. Empty means the
|
|
111
|
+
* promotion is backed by its artifact. Never throws, and never permits: a file
|
|
112
|
+
* that is absent, unreadable, or unparseable is a finding, not a warning —
|
|
113
|
+
* "cannot read the evidence" must not read as "the evidence is good".
|
|
114
|
+
*/
|
|
115
|
+
export function missingArtifacts(checks, projectRoot) {
|
|
116
|
+
const missing = [];
|
|
117
|
+
for (const check of checks) {
|
|
118
|
+
const absolute = resolve(projectRoot, check.path);
|
|
119
|
+
if (!existsSync(absolute)) {
|
|
120
|
+
missing.push(`${check.path} (absent)`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let text;
|
|
124
|
+
try {
|
|
125
|
+
text = readFileSync(absolute, 'utf8');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
missing.push(`${check.path} (unreadable)`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const failure = artifactEvidenceFailure(check, text);
|
|
132
|
+
if (failure !== null) {
|
|
133
|
+
missing.push(`${check.path} (${failure})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return missing;
|
|
137
|
+
}
|
|
44
138
|
const COMMENT_MARKER_RE = /<!--\s*peaks-feedback-promoted:\s*layer=([ABC])\s*-->/;
|
|
139
|
+
/**
|
|
140
|
+
* rid 2026-09-14-gate-h-promotion (classify slice) — the "not to be promoted"
|
|
141
|
+
* declaration.
|
|
142
|
+
*
|
|
143
|
+
* The gate used to know only `has artifact` / `has no artifact`, so a memory that
|
|
144
|
+
* prescribes no action could never pass: promoting it registers a SOP whose only
|
|
145
|
+
* gate is "the source file still exists", which asserts nothing about behaviour.
|
|
146
|
+
* That is a permanent false positive — the old vacuity defect facing the other way.
|
|
147
|
+
*
|
|
148
|
+
* The declaration closes it, but it must not become a way to silence the gate.
|
|
149
|
+
* It differs from the refused grandfather channel (`promotedAt` older than this
|
|
150
|
+
* rule) in that a grandfather exemption is a property of a memory's AGE: every
|
|
151
|
+
* legacy memory has it, it says nothing about content, and nobody has to assert
|
|
152
|
+
* or defend it. This is a bounded claim about the memory's CONTENT:
|
|
153
|
+
*
|
|
154
|
+
* 1. The code is drawn from a closed vocabulary — free text cannot be used.
|
|
155
|
+
* 2. Each code binds to a predicate over the memory's own frontmatter, which
|
|
156
|
+
* the gate recomputes. The declaration may only RESTATE what the memory
|
|
157
|
+
* already says; it cannot introduce a new fact.
|
|
158
|
+
* 3. A reason string is required (the `closedAt` escape hatch beside it has none).
|
|
159
|
+
* 4. Coexisting with a promotion marker is a contradiction and fails, so the
|
|
160
|
+
* channel cannot be used to bury a promotion whose artifact is missing.
|
|
161
|
+
* 5. Exempted memories stay REPORTED via `listPromotionExempt`, so the
|
|
162
|
+
* unpromoted count never drops silently.
|
|
163
|
+
*/
|
|
164
|
+
export const NOT_TO_PROMOTE_CODES = ['non-actionable', 'closed-slice-note'];
|
|
165
|
+
/**
|
|
166
|
+
* What each code's predicate requires the memory to already say. The gate does not
|
|
167
|
+
* and cannot verify that "prescribes no action" is TRUE; it verifies that the
|
|
168
|
+
* declaration agrees with a claim the memory makes on its own.
|
|
169
|
+
*/
|
|
170
|
+
const NOT_TO_PROMOTE_CORROBORATION = {
|
|
171
|
+
'non-actionable': 'frontmatter `scope:` containing `non-actionable`, or `nonActionable: true`',
|
|
172
|
+
'closed-slice-note': 'frontmatter `sourceArtifact:` or `source:` naming the slice it was derived from'
|
|
173
|
+
};
|
|
45
174
|
/**
|
|
46
175
|
* Parse a single `.peaks/memory/<file>.md` into a FeedbackMemory, or
|
|
47
176
|
* `null` when the file is missing / unreadable / not a feedback memory.
|
|
@@ -162,12 +291,49 @@ export function listUnpromotedFeedback(opts) {
|
|
|
162
291
|
// honours the closed state and reports `0 unpromoted` for closed records.
|
|
163
292
|
if (isClosedMemory(join(memoryDir, entry.name)))
|
|
164
293
|
continue;
|
|
294
|
+
// rid 2026-09-14-gate-h-promotion (classify slice): a memory may declare
|
|
295
|
+
// itself out of the gate. A malformed declaration is a FAILURE, not a skip —
|
|
296
|
+
// otherwise "make the fields unusable" would be the quietest way through.
|
|
297
|
+
const declaration = readNotToPromote(join(memoryDir, entry.name));
|
|
298
|
+
if (declaration.kind === 'invalid') {
|
|
299
|
+
out.push({
|
|
300
|
+
name: parsed.name,
|
|
301
|
+
path: parsed.path,
|
|
302
|
+
reason: `not-to-promote declaration rejected: ${declaration.reason}`
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (declaration.kind === 'valid') {
|
|
307
|
+
if (parsed.promotion !== null) {
|
|
308
|
+
out.push({
|
|
309
|
+
name: parsed.name,
|
|
310
|
+
path: parsed.path,
|
|
311
|
+
reason: `carries both a layer ${parsed.promotion.layer} promotion marker and a notToPromote: ${declaration.code} declaration — one of the two claims is false; remove one`
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Deliberately exempt; `listPromotionExempt` reports it so the count is visible.
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
165
318
|
if (parsed.promotion === null) {
|
|
166
319
|
out.push({
|
|
167
320
|
name: parsed.name,
|
|
168
321
|
path: parsed.path,
|
|
169
322
|
reason: 'no promotion marker (comment or sidecar) found — see `peaks feedback promote`'
|
|
170
323
|
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// rid 2026-09-14-gate-h-promotion: a marker is a claim, not evidence. It
|
|
327
|
+
// counts only when the layer's enforcement surface actually carries the
|
|
328
|
+
// artifact. Before this, the marker alone was accepted, so the gate
|
|
329
|
+
// certified whatever the promote command had written and nothing else.
|
|
330
|
+
const missing = missingArtifacts(promotionArtifactChecks(parsed.name, parsed.promotion.layer), opts.projectRoot);
|
|
331
|
+
if (missing.length > 0) {
|
|
332
|
+
out.push({
|
|
333
|
+
name: parsed.name,
|
|
334
|
+
path: parsed.path,
|
|
335
|
+
reason: `marker claims layer ${parsed.promotion.layer} but the artifact is missing: ${missing.join('; ')}`
|
|
336
|
+
});
|
|
171
337
|
}
|
|
172
338
|
}
|
|
173
339
|
return out;
|
|
@@ -209,6 +375,109 @@ function isClosedMemory(filePath) {
|
|
|
209
375
|
}
|
|
210
376
|
return false;
|
|
211
377
|
}
|
|
378
|
+
/** Raw frontmatter text of a memory, or `null` when absent/unreadable. */
|
|
379
|
+
function readFrontmatter(filePath) {
|
|
380
|
+
if (!existsSync(filePath))
|
|
381
|
+
return null;
|
|
382
|
+
let raw;
|
|
383
|
+
try {
|
|
384
|
+
raw = readFileSync(filePath, 'utf8');
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const normalized = raw.replace(/\r\n/g, '\n');
|
|
390
|
+
if (!normalized.startsWith('---\n'))
|
|
391
|
+
return null;
|
|
392
|
+
const endIndex = normalized.indexOf('\n---\n', 4);
|
|
393
|
+
if (endIndex < 0)
|
|
394
|
+
return null;
|
|
395
|
+
return normalized.slice(4, endIndex);
|
|
396
|
+
}
|
|
397
|
+
/** Flat `key: value` lookup over frontmatter text; `null` when unset or empty. */
|
|
398
|
+
function frontmatterValue(frontmatter, key) {
|
|
399
|
+
for (const rawLine of frontmatter.split('\n')) {
|
|
400
|
+
const line = rawLine.trim();
|
|
401
|
+
if (!line.startsWith(`${key}:`))
|
|
402
|
+
continue;
|
|
403
|
+
const value = line.slice(key.length + 1).trim();
|
|
404
|
+
if (value.length === 0 || value === '""' || value === "''")
|
|
405
|
+
return null;
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
/** Does the memory's own frontmatter already say what `code` claims? */
|
|
411
|
+
function corroborates(code, frontmatter) {
|
|
412
|
+
if (code === 'non-actionable') {
|
|
413
|
+
const scope = frontmatterValue(frontmatter, 'scope');
|
|
414
|
+
if (scope !== null && scope.includes('non-actionable'))
|
|
415
|
+
return true;
|
|
416
|
+
return frontmatterValue(frontmatter, 'nonActionable') === 'true';
|
|
417
|
+
}
|
|
418
|
+
return frontmatterValue(frontmatter, 'sourceArtifact') !== null
|
|
419
|
+
|| frontmatterValue(frontmatter, 'source') !== null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Read the memory's not-to-promote declaration. `invalid` is returned rather than
|
|
423
|
+
* `none` when the fields are present but unusable, so the gate fails with a reason
|
|
424
|
+
* instead of quietly treating a malformed declaration as "no declaration".
|
|
425
|
+
*/
|
|
426
|
+
export function readNotToPromote(filePath) {
|
|
427
|
+
const frontmatter = readFrontmatter(filePath);
|
|
428
|
+
if (frontmatter === null)
|
|
429
|
+
return { kind: 'none' };
|
|
430
|
+
const code = frontmatterValue(frontmatter, 'notToPromote');
|
|
431
|
+
const reason = frontmatterValue(frontmatter, 'notToPromoteReason');
|
|
432
|
+
if (code === null && reason === null)
|
|
433
|
+
return { kind: 'none' };
|
|
434
|
+
if (code === null) {
|
|
435
|
+
return { kind: 'invalid', reason: '`notToPromoteReason` is set but the `notToPromote` code is missing' };
|
|
436
|
+
}
|
|
437
|
+
if (!NOT_TO_PROMOTE_CODES.includes(code)) {
|
|
438
|
+
return {
|
|
439
|
+
kind: 'invalid',
|
|
440
|
+
reason: `\`notToPromote: ${code}\` is not a recognised code (expected ${NOT_TO_PROMOTE_CODES.join(' | ')})`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (reason === null) {
|
|
444
|
+
return { kind: 'invalid', reason: `\`notToPromote: ${code}\` has no \`notToPromoteReason\` — an exemption must state its own reason` };
|
|
445
|
+
}
|
|
446
|
+
const typedCode = code;
|
|
447
|
+
if (!corroborates(typedCode, frontmatter)) {
|
|
448
|
+
return {
|
|
449
|
+
kind: 'invalid',
|
|
450
|
+
reason: `\`notToPromote: ${typedCode}\` is not corroborated by the memory's own frontmatter (needs ${NOT_TO_PROMOTE_CORROBORATION[typedCode]})`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { kind: 'valid', code: typedCode, reason };
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The feedback memories that declare themselves out of the gate. Exposed so the
|
|
457
|
+
* gate can REPORT them: an exemption nobody can see is the vacuity this whole
|
|
458
|
+
* channel is required to avoid. Never throws.
|
|
459
|
+
*/
|
|
460
|
+
export function listPromotionExempt(opts) {
|
|
461
|
+
const memoryDir = resolve(opts.projectRoot, '.peaks', 'memory');
|
|
462
|
+
if (!existsSync(memoryDir))
|
|
463
|
+
return [];
|
|
464
|
+
const out = [];
|
|
465
|
+
for (const entry of readdirSync(memoryDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
466
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('.'))
|
|
467
|
+
continue;
|
|
468
|
+
const filePath = join(memoryDir, entry.name);
|
|
469
|
+
const parsed = parseFeedbackMemory(filePath);
|
|
470
|
+
if (parsed === null)
|
|
471
|
+
continue;
|
|
472
|
+
const declaration = readNotToPromote(filePath);
|
|
473
|
+
if (declaration.kind !== 'valid')
|
|
474
|
+
continue;
|
|
475
|
+
if (parsed.promotion !== null)
|
|
476
|
+
continue; // contradiction — reported as a violation instead
|
|
477
|
+
out.push({ name: parsed.name, path: parsed.path, code: declaration.code, reason: declaration.reason });
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
212
481
|
/**
|
|
213
482
|
* Generate the code stub for a given layer. Returns a Markdown
|
|
214
483
|
* snippet the LLM / human can paste into the appropriate file. Pure
|
|
@@ -219,8 +488,8 @@ export function generatePromotionStub(opts) {
|
|
|
219
488
|
const { layer, feedbackName } = opts;
|
|
220
489
|
if (layer === 'A') {
|
|
221
490
|
return {
|
|
222
|
-
snippet: `# SOP entry for feedback "${feedbackName}"\n\n<!-- Append the rule + acceptance criteria below. Reference from a new peaks-sop gate. -->\n\n## Rule\n\n${opts.feedbackBody.split('\n').slice(0, RULE_BODY_PREVIEW_LINES).join('\n')}\n\n## Enforcement\n\
|
|
223
|
-
targetFiles: [
|
|
491
|
+
snippet: `# SOP entry for feedback "${feedbackName}"\n\n<!-- Append the rule + acceptance criteria below. Reference from a new peaks-sop gate. -->\n\n## Rule\n\n${opts.feedbackBody.split('\n').slice(0, RULE_BODY_PREVIEW_LINES).join('\n')}\n\n## Enforcement\n\nAuthor the rule's gates in the generated manifest and reference it from .claude/rules/.`,
|
|
492
|
+
targetFiles: [`.peaks/sops/${sopIdForFeedback(feedbackName)}/sop.json`]
|
|
224
493
|
};
|
|
225
494
|
}
|
|
226
495
|
if (layer === 'B') {
|
|
@@ -235,6 +504,37 @@ export function generatePromotionStub(opts) {
|
|
|
235
504
|
targetFiles: ['src/services/code/mode-gate.ts', `tests/unit/services/code/${feedbackName}-hard-floor.test.ts`]
|
|
236
505
|
};
|
|
237
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Materialize the layer-A artifact: the SOP manifest the engine reads, plus its
|
|
509
|
+
* registry entry. Registration is not optional — `gate-enforce-service` walks
|
|
510
|
+
* `readRegistry()`, so an unregistered manifest enforces nothing.
|
|
511
|
+
*
|
|
512
|
+
* Returns the paths written. `registerSop` lints the manifest first, so a
|
|
513
|
+
* malformed generation throws here rather than leaving a promotion that only
|
|
514
|
+
* looks real.
|
|
515
|
+
*/
|
|
516
|
+
async function generateLayerAArtifact(parsed, projectRoot) {
|
|
517
|
+
const id = sopIdForFeedback(parsed.name);
|
|
518
|
+
const manifestPath = projectSopManifestPath(projectRoot, id);
|
|
519
|
+
const description = parsed.frontmatter.description ?? '';
|
|
520
|
+
const manifest = {
|
|
521
|
+
id,
|
|
522
|
+
name: parsed.name,
|
|
523
|
+
description: `Promoted from feedback memory .peaks/memory/${parsed.name}.md${description.length > 0 ? `: ${description}` : ''}`,
|
|
524
|
+
phases: ['apply'],
|
|
525
|
+
gates: [
|
|
526
|
+
{
|
|
527
|
+
id: 'rule-source-present',
|
|
528
|
+
phase: 'apply',
|
|
529
|
+
check: { type: 'file-exists', path: `.peaks/memory/${parsed.name}.md` }
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
534
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
535
|
+
await registerSop({ id, projectRoot });
|
|
536
|
+
return [manifestPath, projectRegistryPath(projectRoot)];
|
|
537
|
+
}
|
|
238
538
|
/**
|
|
239
539
|
* Write the promotion marker + sidecar. Also writes the envelope to
|
|
240
540
|
* `.peaks/_runtime/<sid>/rd/feedback-promote-<name>.json` for QA
|
|
@@ -245,28 +545,39 @@ export function generatePromotionStub(opts) {
|
|
|
245
545
|
* machine-readable mirror. Both are written; either alone is
|
|
246
546
|
* enough for the scanner.
|
|
247
547
|
*/
|
|
248
|
-
export function promoteFeedback(opts) {
|
|
548
|
+
export async function promoteFeedback(opts) {
|
|
249
549
|
const parsed = parseFeedbackMemory(opts.feedbackPath);
|
|
250
550
|
if (parsed === null) {
|
|
251
551
|
throw new Error(`Not a feedback memory: ${opts.feedbackPath}`);
|
|
252
552
|
}
|
|
553
|
+
// rid 2026-09-14-gate-h-promotion (classify slice): the tool must not create the
|
|
554
|
+
// contradiction the gate rejects — promoting a memory that declares itself out of
|
|
555
|
+
// the gate would print `effective: true` while Gate H reports a contradiction.
|
|
556
|
+
const declaration = readNotToPromote(opts.feedbackPath);
|
|
557
|
+
if (declaration.kind === 'valid') {
|
|
558
|
+
throw new Error(`${opts.feedbackPath} declares \`notToPromote: ${declaration.code}\` — promoting it would contradict that declaration. Remove the declaration first if the memory is a rule after all.`);
|
|
559
|
+
}
|
|
253
560
|
const stub = generatePromotionStub({
|
|
254
561
|
layer: opts.layer,
|
|
255
562
|
feedbackName: parsed.name,
|
|
256
563
|
feedbackBody: parsed.body
|
|
257
564
|
});
|
|
258
565
|
const now = new Date().toISOString();
|
|
566
|
+
const required = promotionArtifactChecks(parsed.name, opts.layer);
|
|
259
567
|
const envelope = {
|
|
260
568
|
name: parsed.name,
|
|
261
569
|
feedbackPath: parsed.path,
|
|
262
570
|
layer: opts.layer,
|
|
263
571
|
layerDetail: PROMOTION_LAYER_DETAILS.find((l) => l.layer === opts.layer)?.label ?? opts.layer,
|
|
264
|
-
generatedFiles:
|
|
572
|
+
generatedFiles: [],
|
|
573
|
+
requiredArtifacts: required.map((check) => check.path),
|
|
574
|
+
effective: false,
|
|
265
575
|
snippet: stub.snippet,
|
|
266
576
|
promotedAt: now,
|
|
267
577
|
promotedBy: opts.promotedBy
|
|
268
578
|
};
|
|
269
579
|
if (opts.dryRun === true) {
|
|
580
|
+
envelope.effective = missingArtifacts(required, opts.projectRoot).length === 0;
|
|
270
581
|
return envelope;
|
|
271
582
|
}
|
|
272
583
|
// 1. Embed comment marker in the memory file.
|
|
@@ -283,23 +594,33 @@ export function promoteFeedback(opts) {
|
|
|
283
594
|
: `${marker}\n${body}`;
|
|
284
595
|
const newContent = normalized.slice(0, endIndex + '\n---\n'.length) + newBody;
|
|
285
596
|
writeFileSync(opts.feedbackPath, newContent, 'utf8');
|
|
597
|
+
envelope.generatedFiles.push(opts.feedbackPath);
|
|
598
|
+
}
|
|
599
|
+
// 2. Layer A: the promotion only means something once the SOP engine can see
|
|
600
|
+
// it. Generated before the sidecar so the sidecar records the full list.
|
|
601
|
+
if (opts.layer === 'A') {
|
|
602
|
+
envelope.generatedFiles.push(...(await generateLayerAArtifact(parsed, opts.projectRoot)));
|
|
286
603
|
}
|
|
287
|
-
//
|
|
604
|
+
// 3. Sidecar (machine-readable mirror).
|
|
288
605
|
const sidecarPath = opts.feedbackPath.replace(/\.md$/, '.promotion.json');
|
|
289
606
|
writeFileSync(sidecarPath, JSON.stringify({
|
|
290
607
|
name: parsed.name,
|
|
291
608
|
layer: opts.layer,
|
|
292
609
|
layerDetail: envelope.layerDetail,
|
|
293
|
-
generatedFiles:
|
|
610
|
+
generatedFiles: envelope.generatedFiles,
|
|
611
|
+
requiredArtifacts: envelope.requiredArtifacts,
|
|
294
612
|
promotedAt: now,
|
|
295
613
|
promotedBy: opts.promotedBy
|
|
296
614
|
}, null, 2), 'utf8');
|
|
297
|
-
|
|
615
|
+
envelope.generatedFiles.push(sidecarPath);
|
|
616
|
+
// 4. Envelope to `.peaks/_runtime/<sid>/rd/`.
|
|
298
617
|
const envelopePath = join(opts.projectRoot, '.peaks', '_runtime', opts.sessionId, 'rd', `feedback-promote-${parsed.name}.json`);
|
|
299
618
|
const envelopeDir = dirname(envelopePath);
|
|
300
619
|
if (!existsSync(envelopeDir)) {
|
|
301
620
|
mkdirSync(envelopeDir, { recursive: true });
|
|
302
621
|
}
|
|
622
|
+
envelope.generatedFiles.push(envelopePath);
|
|
623
|
+
envelope.effective = missingArtifacts(required, opts.projectRoot).length === 0;
|
|
303
624
|
writeFileSync(envelopePath, JSON.stringify(envelope, null, 2), 'utf8');
|
|
304
625
|
return envelope;
|
|
305
626
|
}
|