peaks-loop 4.0.49 → 4.0.51
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 +38 -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/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/hooks-commands.js +55 -38
- package/dist/cli/commands/share-commands.js +113 -20
- 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/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/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-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
- package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
- package/dist/services/ide/adapters/codex-adapter.js +1 -2
- package/dist/services/ide/adapters/cursor-adapter.js +1 -2
- package/dist/services/ide/adapters/hermes-adapter.js +1 -2
- package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
- package/dist/services/ide/adapters/qoder-adapter.js +1 -2
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
- package/dist/services/ide/adapters/trae-adapter.js +1 -2
- package/dist/services/ide/adapters/zcode-adapter.js +0 -1
- package/dist/services/ide/ide-types.d.ts +0 -2
- package/dist/services/observability/observability-service.d.ts +1 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- 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/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/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
- package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +3 -3
- package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
- package/skills/bee/peaks-prd/references/workflow.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +6 -7
- package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
- package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
- package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -3
- package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
- package/skills/bee/peaks-rd/references/external-references.md +1 -1
- package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
- package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
- package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +3 -3
- package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-txt/references/context-capsule.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 +20 -18
- package/skills/peaks-code/references/context-governance.md +1 -1
- package/skills/peaks-code/references/dag-orchestrator.md +3 -4
- package/skills/peaks-code/references/external-references.md +1 -1
- package/skills/peaks-code/references/external-skill-invocation.md +2 -2
- package/skills/peaks-code/references/fanout-mandatory.md +3 -3
- package/skills/peaks-code/references/frontend-only-mode.md +2 -2
- package/skills/peaks-code/references/gstack-integration.md +1 -1
- package/skills/peaks-code/references/micro-cycle.md +1 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/project-scan-checklist.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +1 -1
- package/skills/peaks-code/references/runbook.md +6 -3
- package/skills/peaks-code/references/session-overload-signal-index.md +6 -4
- package/skills/peaks-code/references/startup-sequence.md +17 -17
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +2 -2
- package/skills/peaks-code/references/sub-agent-dispatch.md +26 -25
- package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
- package/skills/peaks-code/references/worktree-governance.md +1 -1
- package/skills/peaks-final-review/SKILL.md +3 -3
- package/skills/peaks-ide/references/audit-log-helper.md +5 -4
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +4 -4
- package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
- package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
- package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
- package/skills/peaks-solo/SKILL.md +1 -2
- 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
|
@@ -1,40 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
4
|
+
import { parseCapabilityMapping, resolveCoverageSummaryPath } from '../../openspec/coverage-evidence-reader.js';
|
|
5
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
6
|
+
const COVERAGE_REL = ['coverage', 'coverage-summary.json'];
|
|
7
|
+
const OPENSPEC_REL = ['openspec', 'coverage-summary.json'];
|
|
8
|
+
function writeJson(root, parts, body) {
|
|
9
|
+
const abs = join(root, ...parts);
|
|
10
|
+
mkdirSync(join(root, ...parts.slice(0, -1)), { recursive: true });
|
|
11
|
+
writeFileSync(abs, JSON.stringify(body));
|
|
12
|
+
}
|
|
13
|
+
const PROPOSAL_WITH_MAPPING = [
|
|
14
|
+
'# change',
|
|
15
|
+
'',
|
|
16
|
+
'## Capability Mapping',
|
|
17
|
+
'',
|
|
18
|
+
'| capability | source | test anchor |',
|
|
19
|
+
'| --- | --- | --- |',
|
|
20
|
+
'| runner | src/services/capability-guard-runner/runner.ts | tests/unit/capability-guard-runner/runner.test.ts |',
|
|
21
|
+
'',
|
|
22
|
+
'## Next section',
|
|
23
|
+
'trailing prose'
|
|
24
|
+
].join('\n');
|
|
25
|
+
/**
|
|
26
|
+
* Behavioural probe of the fixed coverage-summary discovery order.
|
|
27
|
+
*
|
|
28
|
+
* The previous version listed three openspec files and passed if any of them
|
|
29
|
+
* mentioned "Capability Mapping" AND "coverage" — the string "coverage" appears
|
|
30
|
+
* in `coverage-evidence-reader.ts` from its own filename.
|
|
31
|
+
*
|
|
32
|
+
* Here the real resolver is driven against a tmp project where BOTH candidate
|
|
33
|
+
* files exist, then only the second, then neither. Only a resolver that honours
|
|
34
|
+
* the documented order can produce `coverage/` → `openspec/` → missing.
|
|
35
|
+
*/
|
|
9
36
|
export async function runJ15Contract(ctx) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
37
|
+
const row = requireBaselineRow(ctx);
|
|
38
|
+
const missing = missingSourceFiles(ctx, row);
|
|
39
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J15-'));
|
|
40
|
+
try {
|
|
41
|
+
const both = join(root, ...COVERAGE_REL);
|
|
42
|
+
const fallback = join(root, ...OPENSPEC_REL);
|
|
43
|
+
writeJson(root, COVERAGE_REL, { total: { lines: { pct: 100 } } });
|
|
44
|
+
writeJson(root, OPENSPEC_REL, { total: { lines: { pct: 0 } } });
|
|
45
|
+
const first = await resolveCoverageSummaryPath({ projectRoot: root });
|
|
46
|
+
rmSync(both, { force: true });
|
|
47
|
+
const second = await resolveCoverageSummaryPath({ projectRoot: root });
|
|
48
|
+
rmSync(fallback, { force: true });
|
|
49
|
+
const neither = await resolveCoverageSummaryPath({ projectRoot: root });
|
|
50
|
+
const proposalPath = join(root, 'proposal.md');
|
|
51
|
+
writeFileSync(proposalPath, PROPOSAL_WITH_MAPPING);
|
|
52
|
+
const withBlock = await parseCapabilityMapping(proposalPath);
|
|
53
|
+
writeFileSync(proposalPath, '# change\n\n## Other section\n');
|
|
54
|
+
const withoutBlock = await parseCapabilityMapping(proposalPath);
|
|
55
|
+
const triedPaths = !neither.ok && neither.error.code === 'missing' ? neither.error.triedPaths : [];
|
|
56
|
+
const result = combineProbes([
|
|
57
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
58
|
+
probe(first.ok && first.value.replace(/\\/g, '/').endsWith('/coverage/coverage-summary.json'), `with both summaries present the c8 wrapper default wins (${first.ok ? first.value : first.error.code})`),
|
|
59
|
+
probe(second.ok && second.value.replace(/\\/g, '/').endsWith('/openspec/coverage-summary.json'), `with only the openspec summary present the override is used (${second.ok ? second.value : second.error.code})`),
|
|
60
|
+
probe(!neither.ok && neither.error.code === 'missing', `with neither summary present resolution fails (${neither.ok ? 'resolved' : neither.error.code})`),
|
|
61
|
+
probe(triedPaths.length === 2 &&
|
|
62
|
+
triedPaths[0].replace(/\\/g, '/').endsWith('/coverage/coverage-summary.json') &&
|
|
63
|
+
triedPaths[1].replace(/\\/g, '/').endsWith('/openspec/coverage-summary.json'), `the tried-path list reports the fixed order (${triedPaths.join(' | ') || 'none'})`),
|
|
64
|
+
probe(withBlock.present && withBlock.rows.length === 1, `a ## Capability Mapping block parses to one row (present=${String(withBlock.present)} rows=${String(withBlock.rows.length)})`),
|
|
65
|
+
probe(!withoutBlock.present && withoutBlock.rows.length === 0, 'a proposal without the block is reported as absent, not guessed')
|
|
66
|
+
]);
|
|
67
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/openspec/coverage-evidence-reader.ts';
|
|
68
|
+
if (result.ok)
|
|
69
|
+
return pass(ctx, artifact);
|
|
70
|
+
return fail(ctx, artifact, 'coverage-summary discovery order is coverage/ then openspec/, and the Capability Mapping block is parsed not guessed', result.detail, 'J15 invariant broken: the coverage evidence discovery order or the Capability Mapping parse changed');
|
|
19
71
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const src = readFileSync(join(ctx.projectRoot, f), 'utf8');
|
|
23
|
-
return REQUIRED_FRAGMENTS.every((frag) => src.includes(frag));
|
|
24
|
-
});
|
|
25
|
-
if (!fullCoverage) {
|
|
26
|
-
return {
|
|
27
|
-
journeyId: 'J15',
|
|
28
|
-
contract: 'spec-coverage',
|
|
29
|
-
status: 'fail',
|
|
30
|
-
diff: { before: 'openspec services reference both "Capability Mapping" and "coverage"', after: 'no such service', reason: 'J15#1 broken: openspec archive gate is missing the Capability Mapping + coverage cross-check' },
|
|
31
|
-
artifactPath: existing[0]
|
|
32
|
-
};
|
|
72
|
+
finally {
|
|
73
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
74
|
}
|
|
34
|
-
return {
|
|
35
|
-
journeyId: 'J15',
|
|
36
|
-
contract: 'spec-coverage',
|
|
37
|
-
status: 'pass',
|
|
38
|
-
artifactPath: existing[0]
|
|
39
|
-
};
|
|
40
75
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CapabilityBaselineRow } from '../../capability-baseline/types.js';
|
|
2
|
+
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The frozen baseline row this contract is bound to. Throws when the baseline
|
|
5
|
+
* is unreadable or the row is absent — a contract with no baseline behind it
|
|
6
|
+
* must not be able to report green.
|
|
7
|
+
*/
|
|
8
|
+
export declare function requireBaselineRow(ctx: GuardContext): CapabilityBaselineRow;
|
|
9
|
+
/**
|
|
10
|
+
* Every `sourceFiles` entry of the row must still exist. The baseline names the
|
|
11
|
+
* files that carry the journey; a vanished file is drift, so this returns the
|
|
12
|
+
* missing ones rather than filtering them out.
|
|
13
|
+
*/
|
|
14
|
+
export declare function missingSourceFiles(ctx: GuardContext, row: CapabilityBaselineRow): ReadonlyArray<string>;
|
|
15
|
+
export declare function pass(ctx: GuardContext, artifactPath: string): GuardRunResult;
|
|
16
|
+
export declare function fail(ctx: GuardContext, artifactPath: string, before: string, after: string, reason: string): GuardRunResult;
|
|
17
|
+
/** Result of a source-level probe; `ok` drives pass/fail, `detail` the diff. */
|
|
18
|
+
export interface Probe {
|
|
19
|
+
readonly ok: boolean;
|
|
20
|
+
readonly detail: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function combineProbes(probes: ReadonlyArray<Probe>): Probe;
|
|
23
|
+
/** Convenience: build a probe whose ok-ness is a boolean and a label. */
|
|
24
|
+
export declare function probe(ok: boolean, detail: string): Probe;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/services/capability-guard-runner/contracts/_shared.ts
|
|
2
|
+
//
|
|
3
|
+
// Helpers shared by the J01–J15 guard contracts.
|
|
4
|
+
//
|
|
5
|
+
// The point of these contracts is to answer "is this journey's behaviour still
|
|
6
|
+
// right?", not "does some file still contain some word?". The helpers here
|
|
7
|
+
// keep that honest and cheap:
|
|
8
|
+
// - the file list comes from the FROZEN BASELINE ROW, never from a
|
|
9
|
+
// hand-written candidate list inside the contract (so a baseline/source
|
|
10
|
+
// drift is visible instead of being papered over by a fallback path);
|
|
11
|
+
// - a missing source file is a contract failure, not a silently narrower
|
|
12
|
+
// check.
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { readBaselineFile } from '../../capability-baseline/store.js';
|
|
16
|
+
/**
|
|
17
|
+
* The frozen baseline row this contract is bound to. Throws when the baseline
|
|
18
|
+
* is unreadable or the row is absent — a contract with no baseline behind it
|
|
19
|
+
* must not be able to report green.
|
|
20
|
+
*/
|
|
21
|
+
export function requireBaselineRow(ctx) {
|
|
22
|
+
const journeyId = ctx.contract.source.baselineRow;
|
|
23
|
+
const r = readBaselineFile(ctx.projectRoot);
|
|
24
|
+
if (!r.ok) {
|
|
25
|
+
throw new Error(`baseline unreadable at ${ctx.projectRoot} (${r.error.code}: ${r.error.message})`);
|
|
26
|
+
}
|
|
27
|
+
const row = r.file.rows.find((x) => x.journeyId === journeyId);
|
|
28
|
+
if (!row) {
|
|
29
|
+
throw new Error(`baseline row ${journeyId} is not in the frozen baseline`);
|
|
30
|
+
}
|
|
31
|
+
return row;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every `sourceFiles` entry of the row must still exist. The baseline names the
|
|
35
|
+
* files that carry the journey; a vanished file is drift, so this returns the
|
|
36
|
+
* missing ones rather than filtering them out.
|
|
37
|
+
*/
|
|
38
|
+
export function missingSourceFiles(ctx, row) {
|
|
39
|
+
return row.sourceFiles.filter((f) => !existsSync(join(ctx.projectRoot, f)));
|
|
40
|
+
}
|
|
41
|
+
export function pass(ctx, artifactPath) {
|
|
42
|
+
return {
|
|
43
|
+
journeyId: ctx.contract.journeyId,
|
|
44
|
+
contract: ctx.contract.kind,
|
|
45
|
+
status: 'pass',
|
|
46
|
+
artifactPath
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function fail(ctx, artifactPath, before, after, reason) {
|
|
50
|
+
return {
|
|
51
|
+
journeyId: ctx.contract.journeyId,
|
|
52
|
+
contract: ctx.contract.kind,
|
|
53
|
+
status: 'fail',
|
|
54
|
+
diff: { before, after, reason },
|
|
55
|
+
artifactPath
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function combineProbes(probes) {
|
|
59
|
+
const bad = probes.filter((p) => !p.ok);
|
|
60
|
+
if (bad.length === 0)
|
|
61
|
+
return { ok: true, detail: probes.map((p) => p.detail).join('; ') };
|
|
62
|
+
return { ok: false, detail: bad.map((p) => p.detail).join('; ') };
|
|
63
|
+
}
|
|
64
|
+
/** Convenience: build a probe whose ok-ness is a boolean and a label. */
|
|
65
|
+
export function probe(ok, detail) {
|
|
66
|
+
return { ok, detail };
|
|
67
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type JourneyId } from '../capability-baseline/types.js';
|
|
2
|
+
import type { GuardContract } from './types.js';
|
|
3
|
+
export declare const GUARD_CONTRACTS: ReadonlyArray<GuardContract>;
|
|
4
|
+
export declare function getGuardContract(journeyId: JourneyId): GuardContract | undefined;
|
|
5
|
+
export declare function isJourneyId(value: string): value is JourneyId;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/services/capability-guard-runner/registry.ts
|
|
2
|
+
//
|
|
3
|
+
// The single production entry point for the 15 P0 guard contracts.
|
|
4
|
+
//
|
|
5
|
+
// Why a registry: before this, `baseline run-guard` hard-coded `J01`, and the
|
|
6
|
+
// only caller of `runAllGuards` was its own unit test (which passed synthetic
|
|
7
|
+
// contracts against `projectRoot: '/'`). There was no production path that ran
|
|
8
|
+
// all fifteen journeys — so "15 pass / 0 fail" was an assertion nobody made.
|
|
9
|
+
//
|
|
10
|
+
// Each contract declares the baseline row AND the verbatim invariant text it
|
|
11
|
+
// enforces; `assertBaselineRef` resolves that against
|
|
12
|
+
// openspec/baselines/current/capability-baseline.json before the contract runs.
|
|
13
|
+
import { P0_JOURNEY_IDS } from '../capability-baseline/types.js';
|
|
14
|
+
import { runJ01Contract } from './contracts/J01.js';
|
|
15
|
+
import { runJ02Contract } from './contracts/J02.js';
|
|
16
|
+
import { runJ03Contract } from './contracts/J03.js';
|
|
17
|
+
import { runJ04Contract } from './contracts/J04.js';
|
|
18
|
+
import { runJ05Contract } from './contracts/J05.js';
|
|
19
|
+
import { runJ06Contract } from './contracts/J06.js';
|
|
20
|
+
import { runJ07Contract } from './contracts/J07.js';
|
|
21
|
+
import { runJ08Contract } from './contracts/J08.js';
|
|
22
|
+
import { runJ09Contract } from './contracts/J09.js';
|
|
23
|
+
import { runJ10Contract } from './contracts/J10.js';
|
|
24
|
+
import { runJ11Contract } from './contracts/J11.js';
|
|
25
|
+
import { runJ12Contract } from './contracts/J12.js';
|
|
26
|
+
import { runJ13Contract } from './contracts/J13.js';
|
|
27
|
+
import { runJ14Contract } from './contracts/J14.js';
|
|
28
|
+
import { runJ15Contract } from './contracts/J15.js';
|
|
29
|
+
/** The integration test that pins each journey's contract. */
|
|
30
|
+
const ARTIFACTS = {
|
|
31
|
+
J01: 'tests/integration/capability-guard/J01-envelope-arg-shapes.test.ts',
|
|
32
|
+
J02: 'tests/integration/capability-guard/J02-workflow-trace.test.ts',
|
|
33
|
+
J03: 'tests/integration/capability-guard/J03-problem-resolution-flow.test.ts',
|
|
34
|
+
J04: 'tests/integration/capability-guard/J04-audit-goal-binding.test.ts',
|
|
35
|
+
J05: 'tests/integration/capability-guard/J05-final-review-shape.test.ts',
|
|
36
|
+
J06: 'tests/integration/capability-guard/J06-resume-deepest-gate.test.ts',
|
|
37
|
+
J07: 'tests/integration/capability-guard/J07-test-runner-fidelity.test.ts',
|
|
38
|
+
J08: 'tests/integration/capability-guard/J08-asset-roundtrip.test.ts',
|
|
39
|
+
J09: 'tests/integration/capability-guard/J09-sop-register.test.ts',
|
|
40
|
+
J10: 'tests/integration/capability-guard/J10-ide-install-assertion.test.ts',
|
|
41
|
+
J11: 'tests/integration/capability-guard/J11-doctor-cli-snapshot.test.ts',
|
|
42
|
+
J12: 'tests/integration/capability-guard/J12-lease-lifecycle.test.ts',
|
|
43
|
+
J13: 'tests/integration/capability-guard/J13-content-pipeline-trace.test.ts',
|
|
44
|
+
J14: 'tests/integration/capability-guard/J14-issue-orchestrator-trace.test.ts',
|
|
45
|
+
J15: 'tests/integration/capability-guard/J15-spec-coverage.test.ts'
|
|
46
|
+
};
|
|
47
|
+
/** One row per P0 journey. `invariant` must be verbatim baseline text. */
|
|
48
|
+
const SPECS = {
|
|
49
|
+
J01: {
|
|
50
|
+
kind: 'envelope-arg-shapes',
|
|
51
|
+
invariant: 'The CLI verb is never shown to the user as a required input',
|
|
52
|
+
execute: runJ01Contract
|
|
53
|
+
},
|
|
54
|
+
J02: {
|
|
55
|
+
kind: 'workflow-trace',
|
|
56
|
+
invariant: 'Every hard gate (audit / RD / QA / final-review) is enforced; a failure transitions state back to the prior role',
|
|
57
|
+
execute: runJ02Contract
|
|
58
|
+
},
|
|
59
|
+
J03: {
|
|
60
|
+
kind: 'workflow-trace',
|
|
61
|
+
invariant: 'No silent-catch or fake-green pattern is reintroduced in src/services/**',
|
|
62
|
+
execute: runJ03Contract
|
|
63
|
+
},
|
|
64
|
+
J04: {
|
|
65
|
+
kind: 'hook-assertion',
|
|
66
|
+
invariant: 'The audit covers exactly six dimensions: correctness, completeness, scope, risks, alternatives, constraints',
|
|
67
|
+
execute: runJ04Contract
|
|
68
|
+
},
|
|
69
|
+
J05: {
|
|
70
|
+
kind: 'workflow-trace',
|
|
71
|
+
invariant: 'Every final review contains exactly four dimensions: functional-completeness, problem-resolution, no-new-bugs, existing-functionality-intact',
|
|
72
|
+
execute: runJ05Contract
|
|
73
|
+
},
|
|
74
|
+
J06: {
|
|
75
|
+
kind: 'workflow-trace',
|
|
76
|
+
invariant: 'The resume option always identifies the deepest completed gate (audit / RD / QA / final-review)',
|
|
77
|
+
execute: runJ06Contract
|
|
78
|
+
},
|
|
79
|
+
J07: {
|
|
80
|
+
kind: 'cli-output-golden',
|
|
81
|
+
invariant: "Per-test fingerprint cache never returns 'passed' for an unverified file (fileMtime + fileSha256 gate)",
|
|
82
|
+
execute: runJ07Contract
|
|
83
|
+
},
|
|
84
|
+
J08: {
|
|
85
|
+
kind: 'asset-roundtrip',
|
|
86
|
+
invariant: 'Crystallization requires all gates (audit / RD / QA / final-review) to be in a passing state',
|
|
87
|
+
execute: runJ08Contract
|
|
88
|
+
},
|
|
89
|
+
J09: {
|
|
90
|
+
kind: 'sop-register',
|
|
91
|
+
invariant: 'An SOP id must match SOP_ID_PATTERN (lowercase kebab, no dots/slashes) to prevent path traversal',
|
|
92
|
+
execute: runJ09Contract
|
|
93
|
+
},
|
|
94
|
+
J10: {
|
|
95
|
+
kind: 'hook-assertion',
|
|
96
|
+
invariant: 'planHookInstall() runs before applyHookInstall() so the diff is reviewable',
|
|
97
|
+
execute: runJ10Contract
|
|
98
|
+
},
|
|
99
|
+
J11: {
|
|
100
|
+
kind: 'cli-output-golden',
|
|
101
|
+
invariant: 'Doctor checks live under src/services/doctor/doctor-service/checks/ as a code-driven fixed-registry tree',
|
|
102
|
+
execute: runJ11Contract
|
|
103
|
+
},
|
|
104
|
+
J12: {
|
|
105
|
+
kind: 'concurrency-lease',
|
|
106
|
+
invariant: 'Auto-release on terminal heartbeat is best-effort but idempotent: duplicate release is a no-op',
|
|
107
|
+
execute: runJ12Contract
|
|
108
|
+
},
|
|
109
|
+
J13: {
|
|
110
|
+
kind: 'workflow-trace',
|
|
111
|
+
invariant: 'The skill does NOT import peaks-code internals; it reuses the Loop Engineering primitives',
|
|
112
|
+
execute: runJ13Contract
|
|
113
|
+
},
|
|
114
|
+
J14: {
|
|
115
|
+
kind: 'workflow-trace',
|
|
116
|
+
invariant: 'Every fix carries an AI-modified declaration in the commit body (RL-3)',
|
|
117
|
+
execute: runJ14Contract
|
|
118
|
+
},
|
|
119
|
+
J15: {
|
|
120
|
+
kind: 'spec-coverage',
|
|
121
|
+
invariant: 'Coverage-summary discovery order is fixed: <projectRoot>/coverage/coverage-summary.json then /openspec/coverage-summary.json',
|
|
122
|
+
execute: runJ15Contract
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
export const GUARD_CONTRACTS = P0_JOURNEY_IDS.map((journeyId) => {
|
|
126
|
+
const spec = SPECS[journeyId];
|
|
127
|
+
return {
|
|
128
|
+
journeyId,
|
|
129
|
+
kind: spec.kind,
|
|
130
|
+
source: { baselineRow: journeyId, invariant: spec.invariant },
|
|
131
|
+
execute: spec.execute,
|
|
132
|
+
evidence: { kind: spec.kind, artifact: ARTIFACTS[journeyId] }
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
export function getGuardContract(journeyId) {
|
|
136
|
+
return GUARD_CONTRACTS.find((c) => c.journeyId === journeyId);
|
|
137
|
+
}
|
|
138
|
+
export function isJourneyId(value) {
|
|
139
|
+
return P0_JOURNEY_IDS.includes(value);
|
|
140
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import type { CapabilityBaselineRow } from '../capability-baseline/types.js';
|
|
1
2
|
import type { GuardContract, GuardContext, GuardRunResult } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the contract's declared baseline row out of the frozen baseline and
|
|
5
|
+
* assert the declared invariant is verbatim text in that row.
|
|
6
|
+
*
|
|
7
|
+
* Before this, the check was `if (!source.baselineRow || !source.invariant)`,
|
|
8
|
+
* i.e. it only asserted the two fields were truthy. Nothing ever read the
|
|
9
|
+
* baseline, so a contract could name a journey / invariant that does not exist
|
|
10
|
+
* in the frozen file and still run to a green result.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertBaselineRef(contract: GuardContract, projectRoot: string): CapabilityBaselineRow;
|
|
2
13
|
export declare function runGuard(contract: GuardContract, ctx: GuardContext): Promise<GuardRunResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Run every contract and aggregate. A contract that throws (bad baseline
|
|
16
|
+
* reference, unreadable source file, a probe that blows up) is recorded as a
|
|
17
|
+
* `fail` result rather than aborting the whole run — the gate has to be able
|
|
18
|
+
* to say "this journey is broken", not just crash.
|
|
19
|
+
*/
|
|
3
20
|
export declare function runAllGuards(contracts: ReadonlyArray<GuardContract>, ctx: GuardContext): Promise<{
|
|
4
21
|
readonly pass: number;
|
|
5
22
|
readonly fail: number;
|
|
@@ -7,3 +24,12 @@ export declare function runAllGuards(contracts: ReadonlyArray<GuardContract>, ct
|
|
|
7
24
|
readonly total: number;
|
|
8
25
|
readonly results: ReadonlyArray<GuardRunResult>;
|
|
9
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Exit-code contract for a guard summary. `skipped` must not be
|
|
29
|
+
* indistinguishable from `pass`: a run that quietly skipped work is a
|
|
30
|
+
* different outcome from a run that verified work, so it gets its own code.
|
|
31
|
+
*/
|
|
32
|
+
export declare function exitCodeForGuardSummary(summary: {
|
|
33
|
+
readonly fail: number;
|
|
34
|
+
readonly skipped: number;
|
|
35
|
+
}): 0 | 1 | 2;
|
|
@@ -1,16 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/services/capability-guard-runner/runner.ts
|
|
2
|
+
import { readBaselineFile } from '../capability-baseline/store.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the contract's declared baseline row out of the frozen baseline and
|
|
5
|
+
* assert the declared invariant is verbatim text in that row.
|
|
6
|
+
*
|
|
7
|
+
* Before this, the check was `if (!source.baselineRow || !source.invariant)`,
|
|
8
|
+
* i.e. it only asserted the two fields were truthy. Nothing ever read the
|
|
9
|
+
* baseline, so a contract could name a journey / invariant that does not exist
|
|
10
|
+
* in the frozen file and still run to a green result.
|
|
11
|
+
*/
|
|
12
|
+
export function assertBaselineRef(contract, projectRoot) {
|
|
13
|
+
const r = readBaselineFile(projectRoot);
|
|
14
|
+
if (!r.ok) {
|
|
15
|
+
throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} cannot read the frozen baseline at ${projectRoot} (${r.error.code}: ${r.error.message})`);
|
|
4
16
|
}
|
|
17
|
+
const row = r.file.rows.find((x) => x.journeyId === contract.source.baselineRow);
|
|
18
|
+
if (!row) {
|
|
19
|
+
throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} references baseline row ${contract.source.baselineRow}, which is not in the frozen baseline`);
|
|
20
|
+
}
|
|
21
|
+
const declared = contract.source.invariant;
|
|
22
|
+
if (typeof declared !== 'string' || declared.trim().length === 0) {
|
|
23
|
+
throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} declares an empty invariant`);
|
|
24
|
+
}
|
|
25
|
+
if (!row.invariants.includes(declared)) {
|
|
26
|
+
throw new Error(`GUARD_CONTRACT_MISSING_BASELINE_REF: ${contract.journeyId} declares invariant "${declared}", which is not a verbatim invariant of baseline row ${row.journeyId}`);
|
|
27
|
+
}
|
|
28
|
+
return row;
|
|
5
29
|
}
|
|
6
30
|
export async function runGuard(contract, ctx) {
|
|
7
|
-
assertBaselineRef(contract);
|
|
8
|
-
return contract.execute(ctx);
|
|
31
|
+
assertBaselineRef(contract, ctx.projectRoot);
|
|
32
|
+
return contract.execute({ ...ctx, contract });
|
|
9
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Run every contract and aggregate. A contract that throws (bad baseline
|
|
36
|
+
* reference, unreadable source file, a probe that blows up) is recorded as a
|
|
37
|
+
* `fail` result rather than aborting the whole run — the gate has to be able
|
|
38
|
+
* to say "this journey is broken", not just crash.
|
|
39
|
+
*/
|
|
10
40
|
export async function runAllGuards(contracts, ctx) {
|
|
11
41
|
const results = [];
|
|
12
42
|
for (const c of contracts) {
|
|
13
|
-
|
|
43
|
+
try {
|
|
44
|
+
results.push(await runGuard(c, ctx));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
results.push({
|
|
48
|
+
journeyId: c.journeyId,
|
|
49
|
+
contract: c.kind,
|
|
50
|
+
status: 'fail',
|
|
51
|
+
diff: {
|
|
52
|
+
before: 'contract runs against the frozen baseline',
|
|
53
|
+
after: e.message,
|
|
54
|
+
reason: `${c.journeyId} contract could not run`
|
|
55
|
+
},
|
|
56
|
+
artifactPath: c.evidence.artifact
|
|
57
|
+
});
|
|
58
|
+
}
|
|
14
59
|
}
|
|
15
60
|
return {
|
|
16
61
|
pass: results.filter((r) => r.status === 'pass').length,
|
|
@@ -20,3 +65,15 @@ export async function runAllGuards(contracts, ctx) {
|
|
|
20
65
|
results
|
|
21
66
|
};
|
|
22
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Exit-code contract for a guard summary. `skipped` must not be
|
|
70
|
+
* indistinguishable from `pass`: a run that quietly skipped work is a
|
|
71
|
+
* different outcome from a run that verified work, so it gets its own code.
|
|
72
|
+
*/
|
|
73
|
+
export function exitCodeForGuardSummary(summary) {
|
|
74
|
+
if (summary.fail > 0)
|
|
75
|
+
return 1;
|
|
76
|
+
if (summary.skipped > 0)
|
|
77
|
+
return 2;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Slice 2026-07-28 — auto-compact mode table for 24h long-run awareness.
|
|
3
3
|
*
|
|
4
4
|
* The default `'standard'` mode preserves the v2.13.0 zero-pause contract
|
|
5
|
-
* thresholds (preCompact=0.85, redLine=0.95). The `'partial'`
|
|
6
|
-
* earlier for 24h long-run scenarios where the user has explicitly
|
|
5
|
+
* thresholds (autoFire=0.80, preCompact=0.85, redLine=0.95). The `'partial'`
|
|
6
|
+
* mode fires earlier for 24h long-run scenarios where the user has explicitly
|
|
7
7
|
* opted into higher compaction cadence via `peaks session 24h-mode`.
|
|
8
8
|
*
|
|
9
9
|
* Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
|
|
@@ -26,5 +26,16 @@ export declare const AUTO_COMPACT_THRESHOLDS: Readonly<Record<AutoCompactMode, {
|
|
|
26
26
|
}>>;
|
|
27
27
|
export declare function thresholdFor(mode: AutoCompactMode, kind: 'autoFire' | 'preCompact' | 'redLine'): number;
|
|
28
28
|
export declare function isValidMode(value: string): value is AutoCompactMode;
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable form of a mode's threshold row.
|
|
31
|
+
*
|
|
32
|
+
* Derived from `AUTO_COMPACT_THRESHOLDS` rather than hand-written. Diagnosis
|
|
33
|
+
* 2026-09-15 (C1) found this string advertising `0.85/0.95` for `standard`
|
|
34
|
+
* while the table had auto-fired at `0.80` since slice
|
|
35
|
+
* 2026-07-29-context-evaluation-accuracy added the `autoFire` tier — the
|
|
36
|
+
* description named only the two thresholds that were left over from the
|
|
37
|
+
* v2.13.0 contract, and skipped the one that actually fires. Reading the
|
|
38
|
+
* numbers out of the table makes that drift unrepresentable.
|
|
39
|
+
*/
|
|
29
40
|
export declare function describeMode(mode: AutoCompactMode): string;
|
|
30
41
|
export declare function isPartialModeEligible(contextPercent: number): boolean;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Slice 2026-07-28 — auto-compact mode table for 24h long-run awareness.
|
|
3
3
|
*
|
|
4
4
|
* The default `'standard'` mode preserves the v2.13.0 zero-pause contract
|
|
5
|
-
* thresholds (preCompact=0.85, redLine=0.95). The `'partial'`
|
|
6
|
-
* earlier for 24h long-run scenarios where the user has explicitly
|
|
5
|
+
* thresholds (autoFire=0.80, preCompact=0.85, redLine=0.95). The `'partial'`
|
|
6
|
+
* mode fires earlier for 24h long-run scenarios where the user has explicitly
|
|
7
7
|
* opted into higher compaction cadence via `peaks session 24h-mode`.
|
|
8
8
|
*
|
|
9
9
|
* Slice 2026-07-29-context-evaluation-accuracy Part 22: a new
|
|
@@ -28,10 +28,26 @@ export function thresholdFor(mode, kind) {
|
|
|
28
28
|
export function isValidMode(value) {
|
|
29
29
|
return value === 'standard' || value === 'partial';
|
|
30
30
|
}
|
|
31
|
+
function pct(ratio) {
|
|
32
|
+
return `${(ratio * 100).toFixed(0)}%`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Human-readable form of a mode's threshold row.
|
|
36
|
+
*
|
|
37
|
+
* Derived from `AUTO_COMPACT_THRESHOLDS` rather than hand-written. Diagnosis
|
|
38
|
+
* 2026-09-15 (C1) found this string advertising `0.85/0.95` for `standard`
|
|
39
|
+
* while the table had auto-fired at `0.80` since slice
|
|
40
|
+
* 2026-07-29-context-evaluation-accuracy added the `autoFire` tier — the
|
|
41
|
+
* description named only the two thresholds that were left over from the
|
|
42
|
+
* v2.13.0 contract, and skipped the one that actually fires. Reading the
|
|
43
|
+
* numbers out of the table makes that drift unrepresentable.
|
|
44
|
+
*/
|
|
31
45
|
export function describeMode(mode) {
|
|
46
|
+
const { autoFire, preCompact, redLine } = AUTO_COMPACT_THRESHOLDS[mode];
|
|
47
|
+
const bands = `auto-fire ${pct(autoFire)} / pre-compact ${pct(preCompact)} / red-line ${pct(redLine)}`;
|
|
32
48
|
return mode === 'standard'
|
|
33
|
-
?
|
|
34
|
-
:
|
|
49
|
+
? `standard (${bands} — v2.13.0 zero-pause contract)`
|
|
50
|
+
: `partial (${bands} — 24h long-run mode)`;
|
|
35
51
|
}
|
|
36
52
|
export function isPartialModeEligible(contextPercent) {
|
|
37
53
|
return contextPercent >= AUTO_COMPACT_THRESHOLDS.partial.preCompact;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
27
|
+
import { isExpectedFsMiss } from '../../shared/fs-utils.js';
|
|
27
28
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
28
29
|
import { isCodeMode } from './mode-gate.js';
|
|
29
30
|
export const POST_COMPACT_RESUME_REASONS = [
|
|
@@ -65,12 +66,19 @@ function safeReadCheckpoint(absPath) {
|
|
|
65
66
|
const content = mutable;
|
|
66
67
|
return { path: absPath, mtime: stat.mtime, content };
|
|
67
68
|
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
catch (err) {
|
|
70
|
+
// P1 site (S6, 2026-09-15). The TODO(g2) marker this replaces said the
|
|
71
|
+
// catch "narrows to IO errors only" — it did not. It rethrew the two JS
|
|
72
|
+
// error classes it happened to name and swallowed everything else,
|
|
73
|
+
// including the ReferenceError an ESM `require()` bug produces on the one
|
|
74
|
+
// platform this file's whole defence exists for. Corrupt JSON stays loud
|
|
75
|
+
// (we cannot read a checkpoint we cannot parse); every non-fs-miss error
|
|
76
|
+
// now propagates. See `isExpectedFsMiss`.
|
|
71
77
|
if (err instanceof SyntaxError)
|
|
72
|
-
throw err;
|
|
73
|
-
|
|
78
|
+
throw err;
|
|
79
|
+
if (!isExpectedFsMiss(err))
|
|
80
|
+
throw err;
|
|
81
|
+
return null;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
function readLatestTodayCheckpoint(runtimeDir, now) {
|
|
@@ -227,12 +235,13 @@ function readActiveSkillName(projectRoot) {
|
|
|
227
235
|
const presence = getSkillPresence(projectRoot);
|
|
228
236
|
return presence?.skill;
|
|
229
237
|
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
catch (err) {
|
|
239
|
+
// P1 site (S6, 2026-09-15) — same correction as `safeReadCheckpoint`
|
|
240
|
+
// above: a non-fs-miss failure reading the active skill is a bug, not an
|
|
241
|
+
// absence, and no longer resolves to `undefined` (i.e. to "no skill").
|
|
242
|
+
if (!isExpectedFsMiss(err))
|
|
243
|
+
throw err;
|
|
244
|
+
return undefined;
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
247
|
/**
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { existsSync, readFileSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
37
|
import { readJobShapeDecision, JobShapeDecisionError, JOB_SHAPE_NOT_DECIDED } from './job-shape-decision.js';
|
|
38
|
+
import { isExpectedFsMiss } from '../../shared/fs-utils.js';
|
|
38
39
|
export const STEP_08_GATE_FILE_NAME = 'job-shape.json';
|
|
39
40
|
export const STEP_08_PROGRESS_FILE_NAME = 'progress.json';
|
|
40
41
|
/**
|
|
@@ -85,12 +86,16 @@ function readProgressIfAny(projectRoot, sessionId, jid) {
|
|
|
85
86
|
}
|
|
86
87
|
return null;
|
|
87
88
|
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
catch (err) {
|
|
90
|
+
// P1 site (S6, 2026-09-15) — same correction as `readPromptFromLastPromptFile`
|
|
91
|
+
// below: corrupt progress.json stays loud, and a failure that is not an
|
|
92
|
+
// fs miss (module-load ReferenceError, TypeError from a bad field, …)
|
|
93
|
+
// propagates instead of reading as "no progress yet".
|
|
91
94
|
if (err instanceof SyntaxError)
|
|
92
|
-
throw err;
|
|
93
|
-
|
|
95
|
+
throw err;
|
|
96
|
+
if (!isExpectedFsMiss(err))
|
|
97
|
+
throw err;
|
|
98
|
+
return null;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
@@ -100,7 +105,17 @@ function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
|
100
105
|
try {
|
|
101
106
|
return readFileSync(path, 'utf8');
|
|
102
107
|
}
|
|
103
|
-
catch {
|
|
108
|
+
catch (err) {
|
|
109
|
+
// P1 site (S6, 2026-09-15). This catch was bare — it bound nothing and
|
|
110
|
+
// swallowed every error class. The sibling reads in this file had been
|
|
111
|
+
// narrowed; this one had not, so the ONE path that feeds the step-08
|
|
112
|
+
// backup regex was also the one that could not tell "no prompt file" from
|
|
113
|
+
// "the module is broken". On the platform this gate exists for, that is
|
|
114
|
+
// the difference between blocking a disavowal prompt and silently
|
|
115
|
+
// allowing it. Missing/unreadable text is an empty prompt; anything else
|
|
116
|
+
// propagates.
|
|
117
|
+
if (!isExpectedFsMiss(err))
|
|
118
|
+
throw err;
|
|
104
119
|
return '';
|
|
105
120
|
}
|
|
106
121
|
}
|