peaks-loop 4.0.46 → 4.0.48
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 +54 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +139 -16
- package/dist/cli/commands/compact-command.js +241 -1
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +14 -5
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +14 -1
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +14 -3
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +10 -3
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +130 -1
- package/dist/services/code/auto-compact-lifecycle.js +180 -4
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +166 -34
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +75 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +31 -0
- package/dist/services/ide/ide-registry.js +35 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +81 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +9 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +14 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +99 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +91 -14
- package/dist/services/skills/session-start-hook-constants.d.ts +86 -0
- package/dist/services/skills/session-start-hook-constants.js +86 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +31 -12
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +124 -9
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +6 -6
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +8 -6
- package/skills/bee/peaks-qa/SKILL.md +7 -7
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +10 -8
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +3 -3
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* v2.13.2 AC-4 — prd/handoff
|
|
2
|
+
* v2.13.2 AC-4 — prd/handoff-<rid>.md auto-regen on prd:handed-off.
|
|
3
3
|
*
|
|
4
4
|
* When `peaks request transition --role prd --state handed-off` succeeds
|
|
5
|
-
* and
|
|
5
|
+
* and this slice's capsule is missing, this helper writes a sha256-locked
|
|
6
6
|
* handoff (schemaVersion: 2) using the request artifact body as the
|
|
7
|
-
* handoff body. If the
|
|
7
|
+
* handoff body. If the capsule already exists, it's NOT overwritten —
|
|
8
8
|
* the existing handoff is canonical (it may carry a richer body that
|
|
9
9
|
* peaks-prd produced in an earlier session).
|
|
10
10
|
*
|
|
11
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the path carries the rid, so
|
|
12
|
+
* "already exists" is now asked per SLICE. The pre-rid-scoping bare name is
|
|
13
|
+
* deliberately NOT consulted — writing there would recreate the
|
|
14
|
+
* one-slot-per-session collision for the next slice in the session.
|
|
15
|
+
*
|
|
11
16
|
* Karpathy §3 (Surgical Changes): this file only owns the auto-regen
|
|
12
17
|
* path. The other 11 transitions are untouched.
|
|
13
18
|
*/
|
|
@@ -15,7 +20,8 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
15
20
|
import { dirname, join } from 'node:path';
|
|
16
21
|
import { createHash } from 'node:crypto';
|
|
17
22
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
18
|
-
import {
|
|
23
|
+
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
24
|
+
import { handoffRelativePath, sha256OfBody } from './handoff-service.js';
|
|
19
25
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
20
26
|
/**
|
|
21
27
|
* Auto-regen the prd/handoff.md under `.peaks/_runtime/<sid>/prd/`.
|
|
@@ -25,7 +31,7 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
25
31
|
if (opts.role !== 'prd') {
|
|
26
32
|
return { status: 'failed', reason: 'role must be prd' };
|
|
27
33
|
}
|
|
28
|
-
const handoffPath = join(opts.projectRoot,
|
|
34
|
+
const handoffPath = join(opts.projectRoot, handoffRelativePath(opts.sessionId, opts.requestId));
|
|
29
35
|
if (existsSync(handoffPath)) {
|
|
30
36
|
return { status: 'skipped-exists', path: handoffPath };
|
|
31
37
|
}
|
|
@@ -41,28 +47,26 @@ export async function autoRegenPrdHandoff(opts) {
|
|
|
41
47
|
const body = artifact.content;
|
|
42
48
|
const sha256 = sha256OfBody(body);
|
|
43
49
|
// v2.13.3 AC-4 — align with `AUDIT_REQUIRES_HANDOFF` prereq which
|
|
44
|
-
// pins `mustContain: ['schemaVersion: 2', 'sha256:']`.
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
].join('\n');
|
|
65
|
-
const content = `${frontmatter}${body}`;
|
|
50
|
+
// pins `mustContain: ['schemaVersion: 2', 'sha256:']`. Primary field is
|
|
51
|
+
// `sha256`; `handoffHash` is kept as a literal alias for the readers that
|
|
52
|
+
// still use the old key.
|
|
53
|
+
//
|
|
54
|
+
// Slice `2026-09-14-handoff-writer-gate-divergence`: this block used to be
|
|
55
|
+
// hand-rolled here while `handoff-service.serializeHandoff` rendered the
|
|
56
|
+
// SAME contract a second, incompatible way. Both now go through
|
|
57
|
+
// `serializeHandoffFrontmatter`, so the two producers cannot drift again.
|
|
58
|
+
const frontmatter = {
|
|
59
|
+
requestId: opts.requestId,
|
|
60
|
+
sessionId: opts.sessionId,
|
|
61
|
+
schemaVersion: '2',
|
|
62
|
+
handoffHash: sha256,
|
|
63
|
+
writtenAt: new Date().toISOString(),
|
|
64
|
+
goals: [],
|
|
65
|
+
acceptanceCriteria: [],
|
|
66
|
+
preservedBehavior: [],
|
|
67
|
+
handoffPath: normalizePath(handoffPath.replace(opts.projectRoot, '')).replace(/^\//, '')
|
|
68
|
+
};
|
|
69
|
+
const content = `${serializeHandoffFrontmatter(frontmatter)}${body}`;
|
|
66
70
|
mkdirSync(dirname(handoffPath), { recursive: true });
|
|
67
71
|
writeFileSync(handoffPath, content, 'utf8');
|
|
68
72
|
const recomputed = createHash('sha256').update(body, 'utf8').digest('hex');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE canonical serialization of `prd/handoff.md` frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
|
|
5
|
+
* the handoff capsule is written by two producers and read by four
|
|
6
|
+
* consumers, and they did not agree on the bytes:
|
|
7
|
+
*
|
|
8
|
+
* | consumer / producer | requirement |
|
|
9
|
+
* |---|---|
|
|
10
|
+
* | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
|
|
11
|
+
* | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
|
|
12
|
+
* | `handoff-service.readHandoff` | YAML string `handoffHash` |
|
|
13
|
+
* | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
|
|
14
|
+
*
|
|
15
|
+
* `handoff-service.serializeHandoff` fed the frontmatter through
|
|
16
|
+
* `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
|
|
17
|
+
* `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
|
|
18
|
+
* hand-rolled a second serialization that happened to satisfy all four. Two
|
|
19
|
+
* producers, two facts about the same contract, one of them wrong: that is
|
|
20
|
+
* the defect, and copying the right shape into a third place would keep it.
|
|
21
|
+
*
|
|
22
|
+
* So both producers call THIS function. `sha256` is emitted only here, and
|
|
23
|
+
* it is emitted plain, because the two audit loaders anchor a regex on it.
|
|
24
|
+
*
|
|
25
|
+
* Scalar-quoting rule — one rule, one documented exception set:
|
|
26
|
+
* - every ordinary string is emitted through `yamlScalar` (a JSON
|
|
27
|
+
* double-quoted scalar, which is valid YAML 1.2 and round-trips
|
|
28
|
+
* backslashes correctly on Windows paths);
|
|
29
|
+
* - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
|
|
30
|
+
* they are the two scalars the gate and the loaders match textually and a
|
|
31
|
+
* quoted scalar is invisible to all three. Both values are structurally
|
|
32
|
+
* constrained (a version digit and 64 hex chars), so plain is unambiguous.
|
|
33
|
+
*
|
|
34
|
+
* `handoffHash` is quoted even though it is also a 64-hex value: the reader
|
|
35
|
+
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
|
+
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
|
+
*/
|
|
38
|
+
import type { HandoffFrontmatter } from './handoff-types.js';
|
|
39
|
+
/**
|
|
40
|
+
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
41
|
+
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
42
|
+
* the sha256 of the body independent of how the frontmatter renders.
|
|
43
|
+
*/
|
|
44
|
+
export declare function serializeHandoffFrontmatter(frontmatter: HandoffFrontmatter): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE canonical serialization of `prd/handoff.md` frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists (slice `2026-09-14-handoff-writer-gate-divergence`):
|
|
5
|
+
* the handoff capsule is written by two producers and read by four
|
|
6
|
+
* consumers, and they did not agree on the bytes:
|
|
7
|
+
*
|
|
8
|
+
* | consumer / producer | requirement |
|
|
9
|
+
* |---|---|
|
|
10
|
+
* | `AUDIT_REQUIRES_HANDOFF` (`artifact-prerequisites.ts`) | SUBSTRING `schemaVersion: 2` (unquoted) + `sha256:` |
|
|
11
|
+
* | `audit-independent/{security,perf}-audit-service.ts` | anchored `^schemaVersion:\s*(\d+)\s*$` and `^sha256:\s*([a-f0-9]{64})\s*$` |
|
|
12
|
+
* | `handoff-service.readHandoff` | YAML string `handoffHash` |
|
|
13
|
+
* | `handoff-service.verifyHandoff` | `handoffHash` === sha256(body), bare hex |
|
|
14
|
+
*
|
|
15
|
+
* `handoff-service.serializeHandoff` fed the frontmatter through
|
|
16
|
+
* `yaml.stringify`, which emits `schemaVersion: "2"` and a bare
|
|
17
|
+
* `handoffHash:` — failing the gate AND both loaders. `handoff-auto-regen.ts`
|
|
18
|
+
* hand-rolled a second serialization that happened to satisfy all four. Two
|
|
19
|
+
* producers, two facts about the same contract, one of them wrong: that is
|
|
20
|
+
* the defect, and copying the right shape into a third place would keep it.
|
|
21
|
+
*
|
|
22
|
+
* So both producers call THIS function. `sha256` is emitted only here, and
|
|
23
|
+
* it is emitted plain, because the two audit loaders anchor a regex on it.
|
|
24
|
+
*
|
|
25
|
+
* Scalar-quoting rule — one rule, one documented exception set:
|
|
26
|
+
* - every ordinary string is emitted through `yamlScalar` (a JSON
|
|
27
|
+
* double-quoted scalar, which is valid YAML 1.2 and round-trips
|
|
28
|
+
* backslashes correctly on Windows paths);
|
|
29
|
+
* - `schemaVersion` and `sha256` are the exception: emitted PLAIN, because
|
|
30
|
+
* they are the two scalars the gate and the loaders match textually and a
|
|
31
|
+
* quoted scalar is invisible to all three. Both values are structurally
|
|
32
|
+
* constrained (a version digit and 64 hex chars), so plain is unambiguous.
|
|
33
|
+
*
|
|
34
|
+
* `handoffHash` is quoted even though it is also a 64-hex value: the reader
|
|
35
|
+
* parses the block through YAML and requires a STRING, and a bare all-digit
|
|
36
|
+
* sha256 would parse as a YAML number and be refused by the shape check.
|
|
37
|
+
*/
|
|
38
|
+
/** Render a string as a YAML double-quoted scalar. JSON string escapes are a
|
|
39
|
+
* subset of YAML 1.2's double-quoted escapes, so this is valid YAML and
|
|
40
|
+
* handles `\` (Windows paths), quotes, colons and newlines in one step. */
|
|
41
|
+
function yamlScalar(value) {
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
/** Render `key: []` or a YAML block sequence of quoted scalars. */
|
|
45
|
+
function blockSequence(key, values) {
|
|
46
|
+
if (values.length === 0)
|
|
47
|
+
return [`${key}: []`];
|
|
48
|
+
return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
52
|
+
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
53
|
+
* the sha256 of the body independent of how the frontmatter renders.
|
|
54
|
+
*/
|
|
55
|
+
export function serializeHandoffFrontmatter(frontmatter) {
|
|
56
|
+
const lines = [
|
|
57
|
+
'---',
|
|
58
|
+
`requestId: ${yamlScalar(frontmatter.requestId)}`,
|
|
59
|
+
`sessionId: ${yamlScalar(frontmatter.sessionId)}`,
|
|
60
|
+
// PLAIN by construction (`HandoffSchemaVersion` is the literal '2').
|
|
61
|
+
`schemaVersion: ${frontmatter.schemaVersion}`,
|
|
62
|
+
// PLAIN: this is the field both audit loaders read, via an anchored
|
|
63
|
+
// regex that a quoted scalar would not match.
|
|
64
|
+
`sha256: ${frontmatter.handoffHash}`,
|
|
65
|
+
// Quoted: `readHandoff` requires a YAML string here.
|
|
66
|
+
`handoffHash: ${yamlScalar(frontmatter.handoffHash)}`,
|
|
67
|
+
`writtenAt: ${yamlScalar(frontmatter.writtenAt)}`,
|
|
68
|
+
...blockSequence('goals', frontmatter.goals),
|
|
69
|
+
...blockSequence('acceptanceCriteria', frontmatter.acceptanceCriteria),
|
|
70
|
+
...blockSequence('preservedBehavior', frontmatter.preservedBehavior),
|
|
71
|
+
`handoffPath: ${yamlScalar(frontmatter.handoffPath)}`,
|
|
72
|
+
'---',
|
|
73
|
+
];
|
|
74
|
+
return `${lines.join('\n')}\n`;
|
|
75
|
+
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
4
4
|
*
|
|
5
5
|
* Owns the immutable handoff at
|
|
6
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
6
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
7
|
+
* the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
|
|
8
|
+
* readable through `resolveHandoffPath`):
|
|
7
9
|
*
|
|
8
10
|
* - `initHandoff` — pure; computes sha256 of the body and returns
|
|
9
11
|
* a Handoff whose frontmatter `handoffHash` matches.
|
|
@@ -23,6 +25,43 @@
|
|
|
23
25
|
import type { Handoff, HandoffProbe } from './handoff-types.js';
|
|
24
26
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
25
27
|
export declare function sha256OfBody(body: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The canonical capsule path for ONE SLICE, relative to the project root.
|
|
30
|
+
*
|
|
31
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
|
|
32
|
+
* per SESSION (`prd/handoff.md`), so the second slice's handoff silently
|
|
33
|
+
* overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
|
|
34
|
+
* because it never checked WHOSE rid the file named. Measured on
|
|
35
|
+
* `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
|
|
36
|
+
* capsule left by a different line of work.
|
|
37
|
+
*
|
|
38
|
+
* This is the WRITE target, so it always carries the rid — a consumer-side
|
|
39
|
+
* fallback here would leave a rid-scoped requirement with a bare-name writer,
|
|
40
|
+
* which is the defect shape this slice exists to remove. Readers that must
|
|
41
|
+
* tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare function handoffRelativePath(sessionId: string, requestId: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The capsule a CONSUMER should read for (session, requestId): the rid-scoped
|
|
46
|
+
* path when it is on disk, else the pre-rid-scoping bare name. Returns null
|
|
47
|
+
* when neither exists.
|
|
48
|
+
*
|
|
49
|
+
* Three sessions on disk still hold only the bare file
|
|
50
|
+
* (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
|
|
51
|
+
* `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
|
|
52
|
+
* the gate and for every reader below.
|
|
53
|
+
*
|
|
54
|
+
* `requestId` is optional because the detect-only audit surface reaches its
|
|
55
|
+
* service without one. Such a caller can name only the bare path: a session
|
|
56
|
+
* holding nothing but rid-scoped capsules reports missing rather than picking
|
|
57
|
+
* among its siblings' capsules, which would re-open the cross-slice mix-up
|
|
58
|
+
* this scoping exists to close (fail closed, not "some capsule is there").
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveHandoffPath(opts: {
|
|
61
|
+
projectRoot: string;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
requestId?: string;
|
|
64
|
+
}): string | null;
|
|
26
65
|
/** Pure: produce a Handoff with the frontmatter populated. Hash is
|
|
27
66
|
* computed here; callers MUST NOT pre-populate `handoffHash`. */
|
|
28
67
|
export declare function initHandoff(opts: {
|
|
@@ -33,7 +72,7 @@ export declare function initHandoff(opts: {
|
|
|
33
72
|
goals: readonly string[];
|
|
34
73
|
acceptanceCriteria: readonly string[];
|
|
35
74
|
preservedBehavior: readonly string[];
|
|
36
|
-
/** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff
|
|
75
|
+
/** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff-<rid>.md`. */
|
|
37
76
|
handoffPath?: string;
|
|
38
77
|
}): Handoff;
|
|
39
78
|
/** Write a Handoff to disk. `projectRoot` is the absolute project
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
4
4
|
*
|
|
5
5
|
* Owns the immutable handoff at
|
|
6
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
6
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
7
|
+
* the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
|
|
8
|
+
* readable through `resolveHandoffPath`):
|
|
7
9
|
*
|
|
8
10
|
* - `initHandoff` — pure; computes sha256 of the body and returns
|
|
9
11
|
* a Handoff whose frontmatter `handoffHash` matches.
|
|
@@ -21,20 +23,68 @@
|
|
|
21
23
|
* markdown source — no normalization, no trailing-newline padding.
|
|
22
24
|
*/
|
|
23
25
|
import { createHash } from 'node:crypto';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
24
27
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
25
28
|
import { dirname, join } from 'node:path';
|
|
26
|
-
import { parse as parseYaml
|
|
29
|
+
import { parse as parseYaml } from 'yaml';
|
|
30
|
+
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
27
31
|
/** Required schema version for new handoffs. */
|
|
28
32
|
const HANDOFF_SCHEMA_VERSION = '2';
|
|
29
33
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
30
34
|
export function sha256OfBody(body) {
|
|
31
35
|
return createHash('sha256').update(body, 'utf8').digest('hex');
|
|
32
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The canonical capsule path for ONE SLICE, relative to the project root.
|
|
39
|
+
*
|
|
40
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
|
|
41
|
+
* per SESSION (`prd/handoff.md`), so the second slice's handoff silently
|
|
42
|
+
* overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
|
|
43
|
+
* because it never checked WHOSE rid the file named. Measured on
|
|
44
|
+
* `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
|
|
45
|
+
* capsule left by a different line of work.
|
|
46
|
+
*
|
|
47
|
+
* This is the WRITE target, so it always carries the rid — a consumer-side
|
|
48
|
+
* fallback here would leave a rid-scoped requirement with a bare-name writer,
|
|
49
|
+
* which is the defect shape this slice exists to remove. Readers that must
|
|
50
|
+
* tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
|
|
51
|
+
*/
|
|
52
|
+
export function handoffRelativePath(sessionId, requestId) {
|
|
53
|
+
return join('.peaks', '_runtime', sessionId, 'prd', `handoff-${requestId}.md`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The capsule a CONSUMER should read for (session, requestId): the rid-scoped
|
|
57
|
+
* path when it is on disk, else the pre-rid-scoping bare name. Returns null
|
|
58
|
+
* when neither exists.
|
|
59
|
+
*
|
|
60
|
+
* Three sessions on disk still hold only the bare file
|
|
61
|
+
* (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
|
|
62
|
+
* `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
|
|
63
|
+
* the gate and for every reader below.
|
|
64
|
+
*
|
|
65
|
+
* `requestId` is optional because the detect-only audit surface reaches its
|
|
66
|
+
* service without one. Such a caller can name only the bare path: a session
|
|
67
|
+
* holding nothing but rid-scoped capsules reports missing rather than picking
|
|
68
|
+
* among its siblings' capsules, which would re-open the cross-slice mix-up
|
|
69
|
+
* this scoping exists to close (fail closed, not "some capsule is there").
|
|
70
|
+
*/
|
|
71
|
+
export function resolveHandoffPath(opts) {
|
|
72
|
+
const candidates = [
|
|
73
|
+
...(opts.requestId !== undefined ? [handoffRelativePath(opts.sessionId, opts.requestId)] : []),
|
|
74
|
+
join('.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md')
|
|
75
|
+
];
|
|
76
|
+
for (const relative of candidates) {
|
|
77
|
+
const absolute = join(opts.projectRoot, relative);
|
|
78
|
+
if (existsSync(absolute))
|
|
79
|
+
return absolute;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
33
83
|
/** Pure: produce a Handoff with the frontmatter populated. Hash is
|
|
34
84
|
* computed here; callers MUST NOT pre-populate `handoffHash`. */
|
|
35
85
|
export function initHandoff(opts) {
|
|
36
86
|
const handoffPath = opts.handoffPath ??
|
|
37
|
-
|
|
87
|
+
handoffRelativePath(opts.sessionId, opts.requestId);
|
|
38
88
|
const handoffHash = sha256OfBody(opts.body);
|
|
39
89
|
const frontmatter = {
|
|
40
90
|
requestId: opts.requestId,
|
|
@@ -146,8 +196,11 @@ function parseHandoffContent(content) {
|
|
|
146
196
|
};
|
|
147
197
|
}
|
|
148
198
|
function serializeHandoff(handoff) {
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
// The one canonical frontmatter rendering, shared with
|
|
200
|
+
// `handoff-auto-regen.ts`. `yaml.stringify` used to render this block and
|
|
201
|
+
// emitted `schemaVersion: "2"` + a bare `handoffHash:`, which the
|
|
202
|
+
// `AUDIT_REQUIRES_HANDOFF` gate and both audit loaders all reject.
|
|
203
|
+
return `${serializeHandoffFrontmatter(handoff.frontmatter)}${handoff.body}`;
|
|
151
204
|
}
|
|
152
205
|
/**
|
|
153
206
|
* N1: accept BOTH shapes of `schemaVersion` — the string `'2'` and the bare
|
|
@@ -161,13 +214,33 @@ function serializeHandoff(handoff) {
|
|
|
161
214
|
* readable handoff was satisfied by a handoff the parser would not read, and
|
|
162
215
|
* `peaks prd handoff verify` exited 1 on a healthy file.
|
|
163
216
|
*
|
|
164
|
-
*
|
|
165
|
-
* the
|
|
166
|
-
*
|
|
217
|
+
* Slice `2026-09-14-handoff-writer-gate-divergence` then fixed the other half:
|
|
218
|
+
* the writer no longer emits the quoted form at all (see
|
|
219
|
+
* `handoff-frontmatter.ts`). This tolerance stays because handoffs already on
|
|
220
|
+
* disk were written by the old writer and by hand; the writer fix must not
|
|
221
|
+
* retroactively make them unreadable.
|
|
167
222
|
*/
|
|
168
223
|
function isSchemaVersion2(value) {
|
|
169
224
|
return value === HANDOFF_SCHEMA_VERSION || value === 2;
|
|
170
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* The READER's shape check — deliberately looser than `verifyHandoff`:
|
|
228
|
+
* `handoffHash` must be a string, and nothing about its VALUE is validated here.
|
|
229
|
+
*
|
|
230
|
+
* The accepted-but-unverifiable shape, stated rather than left to be
|
|
231
|
+
* discovered: a capsule carrying `handoffHash: sha256:<hex>` — the form the
|
|
232
|
+
* pre-fix writer and this session's hand-corrected capsules use — is READ by
|
|
233
|
+
* `readHandoff` and can NEVER pass `verifyHandoff`. The verifier compares the
|
|
234
|
+
* value to `sha256OfBody(body)` byte for byte (`:127-135`) and no prefix
|
|
235
|
+
* normalization exists anywhere in this module, so the leading `sha256:`
|
|
236
|
+
* guarantees `hash-mismatch`. ONLY the bare-hex form verifies, which is what
|
|
237
|
+
* every writer now emits (`handoff-frontmatter.ts`).
|
|
238
|
+
*
|
|
239
|
+
* So `readHandoff` succeeding is NOT evidence that a capsule is verifiable —
|
|
240
|
+
* the tolerance above exists so old capsules stay READABLE, not so they become
|
|
241
|
+
* acceptable. Request §三 bullet 3 asked for exactly this confirmation
|
|
242
|
+
* (rid `2026-09-14-handoff-writer-gate-divergence`).
|
|
243
|
+
*/
|
|
171
244
|
function isHandoffFrontmatter(value) {
|
|
172
245
|
if (!value || typeof value !== 'object')
|
|
173
246
|
return false;
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
15
15
|
*
|
|
16
16
|
* Path convention: the file lands at
|
|
17
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
18
|
-
*
|
|
17
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
18
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
|
|
19
|
+
* stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
|
|
19
20
|
* `.peaks/_runtime/current-change`). NEVER write under
|
|
20
21
|
* `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
|
|
21
22
|
*/
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
15
15
|
*
|
|
16
16
|
* Path convention: the file lands at
|
|
17
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
18
|
-
*
|
|
17
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
18
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
|
|
19
|
+
* stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
|
|
19
20
|
* `.peaks/_runtime/current-change`). NEVER write under
|
|
20
21
|
* `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
|
|
21
22
|
*/
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
19
20
|
/** 6-item business checklist (12 Gaps QA perspective). */
|
|
20
21
|
export const QA_BUSINESS_ITEMS = [
|
|
21
22
|
{ id: 'business-flow', question: '这个功能"用起来"对吗?(业务流程顺不顺,操作路径是否反人类,跟现有系统交互有没有断层)' },
|
|
@@ -36,6 +37,14 @@ export function buildEmptyQaReview(requestId, sessionId, now = new Date()) {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
export function getQaReviewDir(projectRoot, sessionId) {
|
|
40
|
+
// Sid axis. Every `peaks qa-business-review|score|accept|reject` subcommand
|
|
41
|
+
// reaches the runtime tree through this one constructor, so one guard here
|
|
42
|
+
// covers the whole family — measured: `--session-id ../../../../…/PWNED`
|
|
43
|
+
// wrote `qa-business-reviews/<rid>.json` outside every project root under an
|
|
44
|
+
// `ok: true` envelope (RD sweep case A17).
|
|
45
|
+
if (isUnsafePathInput(sessionId)) {
|
|
46
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
47
|
+
}
|
|
39
48
|
return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'qa-business-reviews');
|
|
40
49
|
}
|
|
41
50
|
export function getQaReviewPath(projectRoot, sessionId, requestId) {
|
|
@@ -34,7 +34,8 @@ export interface PrecheckOptions {
|
|
|
34
34
|
export interface PrecheckEnvelope {
|
|
35
35
|
readonly ok: boolean;
|
|
36
36
|
readonly overall: LayerStatus;
|
|
37
|
-
|
|
37
|
+
/** `null` when the root `package.json` is missing or unreadable — see `rootVsShared`. */
|
|
38
|
+
readonly rootVersion: string | null;
|
|
38
39
|
readonly strict: boolean;
|
|
39
40
|
readonly snapshotAt: string;
|
|
40
41
|
readonly layers: {
|
|
@@ -37,12 +37,45 @@ function upgrade(result, strict) {
|
|
|
37
37
|
}
|
|
38
38
|
return result;
|
|
39
39
|
}
|
|
40
|
+
/** Read + parse a JSON manifest, mapping both failure modes onto a blocker description. */
|
|
41
|
+
function readPackageJson(path, label) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(path, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
message: `${label} is missing or unreadable at ${path}`,
|
|
50
|
+
remediation: `run \`peaks release precheck\` from a project root that has a readable ${label}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `${label} at ${path} is not valid JSON`,
|
|
60
|
+
remediation: `fix the ${label} syntax; \`peaks release precheck\` cannot gate a version it cannot parse`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
40
64
|
function readRootVersion(projectRoot) {
|
|
41
|
-
const
|
|
65
|
+
const path = join(projectRoot, 'package.json');
|
|
66
|
+
const read = readPackageJson(path, 'root package.json');
|
|
67
|
+
if (!read.ok) {
|
|
68
|
+
return read;
|
|
69
|
+
}
|
|
70
|
+
const pkg = read.value;
|
|
42
71
|
if (typeof pkg.version !== 'string' || !SEMVER_RE.test(pkg.version)) {
|
|
43
|
-
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message: `invalid root package.json#version: ${JSON.stringify(pkg.version)}`,
|
|
75
|
+
remediation: 'set root package.json#version to a clean semver (e.g. 4.0.0) before running `peaks release precheck`'
|
|
76
|
+
};
|
|
44
77
|
}
|
|
45
|
-
return pkg.version;
|
|
78
|
+
return { ok: true, version: pkg.version };
|
|
46
79
|
}
|
|
47
80
|
function rollup(layers, strict) {
|
|
48
81
|
const statuses = Object.values(layers).map((l) => l.status);
|
|
@@ -58,7 +91,16 @@ function rollup(layers, strict) {
|
|
|
58
91
|
// Layer A — rootVsShared (mirrors publish.yml §(A) on-disk gate only)
|
|
59
92
|
// ---------------------------------------------------------------------------
|
|
60
93
|
export function runRootVsShared(opts) {
|
|
61
|
-
const
|
|
94
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
95
|
+
if (!rootRead.ok) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'blocker',
|
|
98
|
+
message: rootRead.message,
|
|
99
|
+
remediation: rootRead.remediation,
|
|
100
|
+
observed: { rootVersion: null, sharedVersion: null }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const rootVersion = rootRead.version;
|
|
62
104
|
const sharedDist = join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'dist', 'version.js');
|
|
63
105
|
let sharedVersion = null;
|
|
64
106
|
let distExists = false;
|
|
@@ -112,7 +154,18 @@ export function runRootVsShared(opts) {
|
|
|
112
154
|
// Layer B — tagCollision (CLI-only; not in publish.yml today)
|
|
113
155
|
// ---------------------------------------------------------------------------
|
|
114
156
|
export function runTagCollision(opts) {
|
|
115
|
-
const
|
|
157
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
158
|
+
if (!rootRead.ok) {
|
|
159
|
+
// Fail closed: without a version there is no tag name to check, so this
|
|
160
|
+
// layer cannot claim "safe to publish".
|
|
161
|
+
return {
|
|
162
|
+
status: 'blocker',
|
|
163
|
+
message: rootRead.message,
|
|
164
|
+
remediation: rootRead.remediation,
|
|
165
|
+
observed: { tagName: null }
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const rootVersion = rootRead.version;
|
|
116
169
|
const tagName = `v${rootVersion}`;
|
|
117
170
|
// 2026-09-10: no shell. `git` is `git.exe` on Windows, so the wrapper bought
|
|
118
171
|
// nothing — and it actively broke this layer, because `projectRoot` is an
|
|
@@ -125,7 +178,8 @@ export function runTagCollision(opts) {
|
|
|
125
178
|
// blocker). It also emitted DEP0190 on every run, on every layer.
|
|
126
179
|
const res = spawnSync('git', ['-C', opts.projectRoot, 'tag', '--list', tagName], {
|
|
127
180
|
encoding: 'utf8',
|
|
128
|
-
timeout: 5_000
|
|
181
|
+
timeout: 5_000,
|
|
182
|
+
windowsHide: true
|
|
129
183
|
});
|
|
130
184
|
if (res.error !== null && res.error !== undefined) {
|
|
131
185
|
return {
|
|
@@ -198,8 +252,16 @@ export function runChangesetStaged(opts) {
|
|
|
198
252
|
// Layer D — workspaceLockstep (CLI-only; not in publish.yml today)
|
|
199
253
|
// ---------------------------------------------------------------------------
|
|
200
254
|
export function runWorkspaceLockstep(opts) {
|
|
201
|
-
const
|
|
202
|
-
|
|
255
|
+
const rootRead = readPackageJson(join(opts.projectRoot, 'package.json'), 'root package.json');
|
|
256
|
+
if (!rootRead.ok) {
|
|
257
|
+
return {
|
|
258
|
+
status: 'blocker',
|
|
259
|
+
message: rootRead.message,
|
|
260
|
+
remediation: rootRead.remediation,
|
|
261
|
+
observed: { sharedDep: null }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const rootPkg = rootRead.value;
|
|
203
265
|
const allDeps = {
|
|
204
266
|
...(rootPkg.dependencies ?? {}),
|
|
205
267
|
...(rootPkg.devDependencies ?? {})
|
|
@@ -222,9 +284,16 @@ export function runWorkspaceLockstep(opts) {
|
|
|
222
284
|
observed: { sharedDep }
|
|
223
285
|
};
|
|
224
286
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
287
|
+
const sharedRead = readPackageJson(join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'package.json'), 'packages/peaks-loop-shared/package.json');
|
|
288
|
+
if (!sharedRead.ok) {
|
|
289
|
+
return {
|
|
290
|
+
status: 'blocker',
|
|
291
|
+
message: sharedRead.message,
|
|
292
|
+
remediation: sharedRead.remediation,
|
|
293
|
+
observed: { sharedDep, sharedVersion: null }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const sharedVersion = sharedRead.value.version ?? '';
|
|
228
297
|
if (!SEMVER_RE.test(sharedVersion)) {
|
|
229
298
|
return {
|
|
230
299
|
status: 'blocker',
|
|
@@ -244,7 +313,8 @@ export function runWorkspaceLockstep(opts) {
|
|
|
244
313
|
// runAllLayers — orchestrator
|
|
245
314
|
// ---------------------------------------------------------------------------
|
|
246
315
|
export function runAllLayers(opts) {
|
|
247
|
-
const
|
|
316
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
317
|
+
const rootVersion = rootRead.ok ? rootRead.version : null;
|
|
248
318
|
const strict = opts.strict === true;
|
|
249
319
|
const layers = {
|
|
250
320
|
rootVsShared: upgrade(runRootVsShared(opts), strict),
|
|
@@ -20,11 +20,36 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Vendor-specific verb strings (`claude --compact`, `codex --compact`,
|
|
22
22
|
* `copilot compact`) MUST live ONLY in adapter implementations under
|
|
23
|
-
* `src/services/runtime/vendors/<vendor>.ts
|
|
23
|
+
* `src/services/runtime/vendors/<vendor>.ts` and
|
|
24
|
+
* `src/services/ide/adapters/<vendor>-adapter.ts`. The runtime service that
|
|
24
25
|
* orchestrates these adapters (runtime-service.ts) and the CLI commands
|
|
25
|
-
* (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs
|
|
26
|
-
*
|
|
27
|
-
* compact
|
|
26
|
+
* (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs.
|
|
27
|
+
*
|
|
28
|
+
* Slice 2026-09-12-auto-compact-vendor-neutrality widened AC-1's scope from
|
|
29
|
+
* `src/services/code/` to ALL of `src/`, and had to change how it is
|
|
30
|
+
* enforced. Both changes came from measurement:
|
|
31
|
+
*
|
|
32
|
+
* - The old scope excluded the files that carried the defect AC-1
|
|
33
|
+
* describes — the compact dispatcher (`src/services/context/`) and the
|
|
34
|
+
* hook installer (`src/services/hooks/`) — so it returned 0 by
|
|
35
|
+
* construction. A grep over a directory that does not contain the code
|
|
36
|
+
* being checked is not evidence of anything.
|
|
37
|
+
* - Widened as a raw grep over `src/`, the check returns 13, not 0. Twelve
|
|
38
|
+
* hits are the phrase quoted in COMMENTS and doc strings (this block
|
|
39
|
+
* among them); the thirteenth is `compactCommand: 'claude --compact'`
|
|
40
|
+
* in `src/services/ide/adapters/claude-code-adapter.ts` — the one place
|
|
41
|
+
* the string is supposed to be. A text grep measures prose, not code.
|
|
42
|
+
*
|
|
43
|
+
* AC-1 is therefore enforced by the AST-based check in
|
|
44
|
+
* `tests/unit/runtime/vendor-neutral-identity-guard.test.ts`: no vendor
|
|
45
|
+
* verb in any STRING LITERAL of `src/` outside the adapter implementations.
|
|
46
|
+
* Parsed, the count is 1 and the single hit is that adapter — which is the
|
|
47
|
+
* property AC-1 was always about.
|
|
48
|
+
*
|
|
49
|
+
* AC-1 covers VERB STRINGS only. The compact path's defects were IDENTITY
|
|
50
|
+
* COMPARISONS (`ideId !== 'claude-code'`), a second shape the same guard
|
|
51
|
+
* file covers and AC-1 never could. Read that guard's header for what
|
|
52
|
+
* neither check catches before treating vendor neutrality as proven.
|
|
28
53
|
*
|
|
29
54
|
* See `.peaks/_runtime/2026-07-08-session-17918f/prd/002-adapter-runtime-and-polyrepo.md`
|
|
30
55
|
* for the source PRD.
|