peaks-loop 4.0.7 → 4.0.8
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 +46 -288
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/observability/observability-service.d.ts +2 -2
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +4 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -4,28 +4,43 @@
|
|
|
4
4
|
* Resolves the active peak skill name for the current session, so hook
|
|
5
5
|
* enforcers (e.g. code-ban) can decide whether to fire.
|
|
6
6
|
*
|
|
7
|
+
* Slice 4.0.8 (D1 + D4a): prefer the canonical
|
|
8
|
+
* `readPresenceLease` projection from `presence-lease-service.ts`.
|
|
9
|
+
* The 4.0.7 legacy `active-skill-<callerId>.json` walk is preserved
|
|
10
|
+
* as a 1-minor-release back-compat fallback, gated on the
|
|
11
|
+
* `legacyPresence: true` opt-in. By default the canonical projection
|
|
12
|
+
* is the only source of truth — the read returns `{ skill, callerId,
|
|
13
|
+
* sessionId, source: 'canonical' | 'legacy' | 'none' }`.
|
|
14
|
+
*
|
|
7
15
|
* Per `src/services/session/caller-id-types.ts`: the active-skill file is
|
|
8
|
-
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json
|
|
16
|
+
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`
|
|
17
|
+
* (legacy) or `.peaks/_runtime/<sid>/presence-index/<callerId>.json`
|
|
18
|
+
* (canonical index → lease file).
|
|
9
19
|
*
|
|
10
20
|
* Resolution order (graceful degradation — never throws):
|
|
11
21
|
* 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
|
|
12
|
-
* 2.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
22
|
+
* 2. Canonical lease projection (presence-lease-service.readPresenceLease)
|
|
23
|
+
* 3. Legacy `.peaks/_runtime/<sid>/active-skill-*.json` walk, when
|
|
24
|
+
* `legacyPresence === true` is passed by the caller
|
|
25
|
+
* 4. null (caller did not set a skill; enforcers can decide to skip)
|
|
15
26
|
*/
|
|
16
27
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
28
|
import { join } from 'node:path';
|
|
18
29
|
import { getSessionIdCanonical } from '../../session/session-manager.js';
|
|
19
30
|
import { getSessionDir } from '../../session/getSessionDir.js';
|
|
31
|
+
import { readPresenceLease } from '../../skills/presence-lease-service.js';
|
|
32
|
+
import { getCurrentSessionId } from '../../skills/skill-presence-service.js';
|
|
20
33
|
const ACTIVE_SKILL_PREFIX = 'active-skill-';
|
|
21
34
|
/**
|
|
22
35
|
* Resolve the active peak skill for the current hook invocation.
|
|
23
36
|
*
|
|
24
|
-
* Reads PEAKS_ACTIVE_SKILL first (test override), then
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
37
|
+
* Reads PEAKS_ACTIVE_SKILL first (test override), then the canonical
|
|
38
|
+
* lease projection (RD §4 / §6 — the lease + index is the source of
|
|
39
|
+
* truth in 4.0.8), then — only when the caller explicitly opts in via
|
|
40
|
+
* `legacyPresence: true` — the legacy `.peaks/_runtime/<sid>/active-skill-*.json`
|
|
41
|
+
* walk. Returns the first match.
|
|
27
42
|
*/
|
|
28
|
-
export function resolveActiveSkillForCaller(projectRoot) {
|
|
43
|
+
export function resolveActiveSkillForCaller(projectRoot, opts) {
|
|
29
44
|
const envOverride = process.env.PEAKS_ACTIVE_SKILL;
|
|
30
45
|
if (typeof envOverride === 'string' && envOverride.length > 0) {
|
|
31
46
|
return { skill: envOverride, callerId: null, sessionId: null, source: 'env' };
|
|
@@ -40,32 +55,79 @@ export function resolveActiveSkillForCaller(projectRoot) {
|
|
|
40
55
|
if (sessionId === null) {
|
|
41
56
|
return { skill: null, callerId: null, sessionId: null, source: 'none' };
|
|
42
57
|
}
|
|
58
|
+
// 4.0.8 preferred: read the canonical lease projection. The lease
|
|
59
|
+
// service exposes `readPresenceLease({ projectRoot, sessionId,
|
|
60
|
+
// callerId, workflowId, graphRef })`; we need *some* callerId to
|
|
61
|
+
// point at a single lease. Hook enforcers don't have a callerId
|
|
62
|
+
// (they're per-IDE, per-window), so we walk the canonical
|
|
63
|
+
// presence-index to find any active lease. When the index is empty
|
|
64
|
+
// (e.g. ad-hoc / pre-migration projects) we fall through to the
|
|
65
|
+
// legacy walk below.
|
|
43
66
|
const sessionDir = getSessionDir(projectRoot, sessionId);
|
|
44
67
|
if (!existsSync(sessionDir)) {
|
|
45
|
-
return { skill: null, callerId: null, sessionId
|
|
68
|
+
return { skill: null, callerId: null, sessionId, source: 'none' };
|
|
46
69
|
}
|
|
47
|
-
let entries;
|
|
70
|
+
let entries = [];
|
|
48
71
|
try {
|
|
49
72
|
entries = readdirSync(sessionDir);
|
|
50
73
|
}
|
|
51
74
|
catch {
|
|
52
|
-
|
|
75
|
+
entries = [];
|
|
53
76
|
}
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
const indexEntries = entries.filter((e) => !e.startsWith(ACTIVE_SKILL_PREFIX) && e.endsWith('.json') && !e.includes('.tmp-'));
|
|
78
|
+
for (const entry of indexEntries) {
|
|
79
|
+
// The canonical presence-index lives at `<sessionDir>/presence-index/<callerId>.json`.
|
|
80
|
+
// We synthesise the read from any presence-* lease file we find;
|
|
81
|
+
// the readPresenceLease call surfaces a typed `PresenceProjection`
|
|
82
|
+
// and we map `lease.skill` → resolution.
|
|
83
|
+
if (!entry.startsWith('presence-'))
|
|
84
|
+
continue;
|
|
85
|
+
const callerId = entry.slice('presence-'.length, -'.json'.length);
|
|
86
|
+
// Derive workflowId from the same file name (canonical
|
|
87
|
+
// leaseFilePath encodes `presence-<caller>-<workflow>.json`).
|
|
88
|
+
const lastDash = callerId.lastIndexOf('-');
|
|
89
|
+
if (lastDash < 0)
|
|
56
90
|
continue;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
91
|
+
const workflowId = callerId.slice(lastDash + 1);
|
|
92
|
+
const pureCaller = callerId.slice(0, lastDash);
|
|
59
93
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
const projection = readPresenceLease({
|
|
95
|
+
projectRoot,
|
|
96
|
+
sessionId,
|
|
97
|
+
callerId: pureCaller,
|
|
98
|
+
workflowId,
|
|
99
|
+
graphRef: `graphs/${workflowId}.json`,
|
|
100
|
+
});
|
|
101
|
+
if (projection.lease !== null && typeof projection.lease.skill === 'string' && projection.lease.skill.length > 0) {
|
|
102
|
+
return { skill: projection.lease.skill, callerId: pureCaller, sessionId, source: 'canonical' };
|
|
64
103
|
}
|
|
65
104
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
105
|
+
catch { /* fall through to legacy walk */ }
|
|
106
|
+
}
|
|
107
|
+
// Legacy fall-back (one minor release). Gated on `legacyPresence:
|
|
108
|
+
// true` so production hooks prefer the canonical projection.
|
|
109
|
+
if (opts?.legacyPresence === true) {
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (!entry.startsWith(ACTIVE_SKILL_PREFIX) || !entry.endsWith('.json'))
|
|
112
|
+
continue;
|
|
113
|
+
const callerId = entry.slice(ACTIVE_SKILL_PREFIX.length, -'.json'.length);
|
|
114
|
+
const filePath = join(sessionDir, entry);
|
|
115
|
+
try {
|
|
116
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
if (typeof parsed.skill === 'string' && parsed.skill.length > 0) {
|
|
119
|
+
return { skill: parsed.skill, callerId, sessionId, source: 'file' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
123
|
+
// skip malformed file
|
|
124
|
+
}
|
|
68
125
|
}
|
|
69
126
|
}
|
|
127
|
+
// Suppress unused-import warnings for symbols reserved for the
|
|
128
|
+
// migration window. `getCurrentSessionId` is kept so the legacy
|
|
129
|
+
// walk can still resolve a session id if the canonical resolver
|
|
130
|
+
// returns null (e.g. ad-hoc projects without a `.peaks/_runtime/session.json`).
|
|
131
|
+
void getCurrentSessionId;
|
|
70
132
|
return { skill: null, callerId: null, sessionId, source: 'none' };
|
|
71
133
|
}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { RED_LINE_CATALOG } from '../red-line-catalog.js';
|
|
18
|
-
import { resolvePeaksLoopRoot } from '../backing-detector.js';
|
|
19
18
|
export const CATALOG_STABILITY_GROWTH_CAP = 0.20;
|
|
20
19
|
export const CATALOG_STABILITY_WINDOW_DAYS = 90;
|
|
21
20
|
export const RUNTIME_BUDGET_MS = 2000;
|
|
@@ -42,19 +41,11 @@ export function lintCatalogStability(input) {
|
|
|
42
41
|
return [syntheticHit('rl-audit-catalog-stability-001', 'catalog size has not grown > 20% in the last 90 days', `(growth ${(growth * 100).toFixed(1)}% over 90 days; currentSize=${input.currentSize}, priorSize=${input.sizeNinetyDaysAgo})`)];
|
|
43
42
|
}
|
|
44
43
|
export function lintNoOrphanEnforcer(projectRoot) {
|
|
45
|
-
// Slice 4.0.7-dogfood-PR-1: catalog enforcerRef paths are written
|
|
46
|
-
// relative to the peaks-loop source root (catalog + enforcers both
|
|
47
|
-
// live at <root>/src/services/audit/...). Resolving against the
|
|
48
|
-
// audited --project root produces false orphans on every downstream
|
|
49
|
-
// project. Use resolvePeaksLoopRoot() and keep `projectRoot` for
|
|
50
|
-
// test/legacy callers (the parameter is intentionally ignored here).
|
|
51
|
-
void projectRoot;
|
|
52
|
-
const peaksLoopRoot = resolvePeaksLoopRoot();
|
|
53
44
|
const hits = [];
|
|
54
45
|
for (const entry of RED_LINE_CATALOG) {
|
|
55
46
|
if (!entry.enforcerRef)
|
|
56
47
|
continue;
|
|
57
|
-
const absPath = join(
|
|
48
|
+
const absPath = join(projectRoot, entry.enforcerRef);
|
|
58
49
|
if (!existsSync(absPath)) {
|
|
59
50
|
hits.push(syntheticHit('rl-audit-no-orphan-enforcer-001', 'every enforcerRef points to a real file', `(enforcerRef "${entry.enforcerRef}" for ${entry.id} does not exist on disk)`));
|
|
60
51
|
}
|
|
@@ -5,8 +5,26 @@ import { type CompactLifecycleRecord, type CompactLifecycleStage } from '../comp
|
|
|
5
5
|
export interface AutoCompactInput {
|
|
6
6
|
/** Project root for context (default cwd). */
|
|
7
7
|
readonly projectRoot: string;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Caller-provided in-flight batch probe (default false). Slice
|
|
10
|
+
* 4.0.8 (D4d): production callers should pass a `probeInflightBatch`
|
|
11
|
+
* function (see below). The legacy `inFlightBatch` boolean
|
|
12
|
+
* remains as a TEST-ONLY seam — the CLI gates it behind
|
|
13
|
+
* `process.env.PEAKS_TEST_SEAM === '1'` so a production run
|
|
14
|
+
* never consults the boolean as truth.
|
|
15
|
+
*/
|
|
9
16
|
readonly inFlightBatch?: InFlightBatchProbe | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Slice 4.0.8 (D4d): graph-only in-flight probe. When supplied,
|
|
19
|
+
* the orchestrator calls this function and uses the boolean
|
|
20
|
+
* result as the production `inFlightBatch` signal. The legacy
|
|
21
|
+
* boolean parameter is suppressed in production callers — the
|
|
22
|
+
* CLI's `--in-flight-batch` flag is gated behind
|
|
23
|
+
* `PEAKS_TEST_SEAM === '1'`. The pure decision function
|
|
24
|
+
* `evaluateAutoCompactDecision` retains the boolean signature
|
|
25
|
+
* for the test seam + the red-line override.
|
|
26
|
+
*/
|
|
27
|
+
readonly probeInflightBatch?: (() => boolean) | undefined;
|
|
10
28
|
/**
|
|
11
29
|
* Force execute even when ratio < threshold (test seam). In
|
|
12
30
|
* production this is always `false` — peaks-loop drives compact
|
|
@@ -96,7 +114,20 @@ export declare function evaluateCompactTrigger(ratio: number, mode?: AutoCompact
|
|
|
96
114
|
*/
|
|
97
115
|
export declare function evaluateAutoCompactDecision(input: {
|
|
98
116
|
ratio: number;
|
|
99
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Caller-provided in-flight batch signal. Accepts either a plain
|
|
119
|
+
* boolean (convenience / repro seam) or the full `InFlightBatchProbe`
|
|
120
|
+
* shape (production callers — graph-probe-backed). When the boolean
|
|
121
|
+
* form is `true`, the normalized probe carries
|
|
122
|
+
* `hasInFlightBatch: true`. Slice 4.0.8 hotfix.
|
|
123
|
+
*/
|
|
124
|
+
inFlightBatch?: boolean | InFlightBatchProbe | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Legacy camelCase alias for `inFlightBatch`. Production repro
|
|
127
|
+
* inputs use `inflightBatch: true|false`; the orchestrator
|
|
128
|
+
* normalizes it to the same internal shape. Slice 4.0.8 hotfix.
|
|
129
|
+
*/
|
|
130
|
+
inflightBatch?: boolean | InFlightBatchProbe | undefined;
|
|
100
131
|
force?: boolean | undefined;
|
|
101
132
|
bypassRedLine?: boolean | undefined;
|
|
102
133
|
mode?: AutoCompactMode | undefined;
|
|
@@ -111,6 +142,8 @@ export declare function evaluateAutoCompactDecision(input: {
|
|
|
111
142
|
shouldCompact: boolean;
|
|
112
143
|
reason: 'below-threshold' | 'in-flight-batch' | 'pre-compact' | 'red-line';
|
|
113
144
|
trigger: CompactTrigger;
|
|
145
|
+
/** Typed verdict enum (RD §4 24h-mode contract). Slice 4.0.8 hotfix. */
|
|
146
|
+
action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' | 'defer';
|
|
114
147
|
};
|
|
115
148
|
/**
|
|
116
149
|
* Build the convergence plan that D7's post-compact-detect will read
|
|
@@ -108,19 +108,29 @@ export function evaluateCompactTrigger(ratio, mode = 'standard') {
|
|
|
108
108
|
*/
|
|
109
109
|
export function evaluateAutoCompactDecision(input) {
|
|
110
110
|
const trigger = evaluateCompactTrigger(input.ratio, input.mode ?? 'standard');
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// Normalize both `inFlightBatch` and `inflightBatch` (boolean | probe)
|
|
112
|
+
// into a single `InFlightBatchProbe` shape. The probe reads
|
|
113
|
+
// `hasInFlightBatch`; the boolean reads truthiness.
|
|
114
|
+
const rawProbe = input.inFlightBatch ?? input.inflightBatch;
|
|
115
|
+
const probe = typeof rawProbe === 'boolean'
|
|
116
|
+
? { hasInFlightBatch: rawProbe }
|
|
117
|
+
: rawProbe;
|
|
118
|
+
if (trigger.kind === 'none') {
|
|
119
|
+
return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'ok' };
|
|
120
|
+
}
|
|
121
|
+
if (trigger.kind === 'soft-warn') {
|
|
122
|
+
return { shouldCompact: false, reason: 'below-threshold', trigger, action: 'soft-warn' };
|
|
113
123
|
}
|
|
114
124
|
if (trigger.kind === 'red-line') {
|
|
115
125
|
// Red line: ignore in-flight batch — synchronous dispatch wins.
|
|
116
|
-
return { shouldCompact: true, reason: 'red-line', trigger };
|
|
126
|
+
return { shouldCompact: true, reason: 'red-line', trigger, action: 'red-line' };
|
|
117
127
|
}
|
|
118
128
|
// pre-compact zone (0.85 ≤ ratio < 0.95): honor D6.e in-flight deferral.
|
|
119
|
-
if (
|
|
120
|
-
return { shouldCompact: false, reason: 'in-flight-batch', trigger };
|
|
129
|
+
if (probe?.hasInFlightBatch === true) {
|
|
130
|
+
return { shouldCompact: false, reason: 'in-flight-batch', trigger, action: 'defer' };
|
|
121
131
|
}
|
|
122
132
|
if (input.force) {
|
|
123
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
133
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
124
134
|
}
|
|
125
135
|
// Slice 2026-07-31-rid-mac-transcript-estimate-trigger: transcript-estimate
|
|
126
136
|
// is the ONLY signal available on Mac Claude Code (no env-var, no statusline
|
|
@@ -131,9 +141,9 @@ export function evaluateAutoCompactDecision(input) {
|
|
|
131
141
|
// source is present (`claude-code-env` would have been P1, `statusline-poll`
|
|
132
142
|
// P2, `user-overridden` P4) — Mac's only signal is `transcript-estimate`.
|
|
133
143
|
if (input.source === 'transcript-estimate' && input.ratio >= AUTO_COMPACT_PRE_COMPACT_RATIO)
|
|
134
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
144
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
135
145
|
// Default: peaks-loop drives pre-compact autonomously.
|
|
136
|
-
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
146
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger, action: 'auto-compact-now' };
|
|
137
147
|
}
|
|
138
148
|
/**
|
|
139
149
|
* Build the convergence plan that D7's post-compact-detect will read
|
|
@@ -514,7 +524,17 @@ export async function runAutoCompact(input) {
|
|
|
514
524
|
});
|
|
515
525
|
const decision = evaluateAutoCompactDecision({
|
|
516
526
|
ratio: probe.ratio,
|
|
517
|
-
|
|
527
|
+
// Slice 4.0.8 (D4d): production `inFlightBatch` MUST come from
|
|
528
|
+
// the graph probe (see `workflow-inflight-probe.ts`). The
|
|
529
|
+
// legacy boolean is preserved as a TEST-ONLY seam: when
|
|
530
|
+
// `probeInflightBatch` is supplied, we call it and pass the
|
|
531
|
+
// result as a `InFlightBatchProbe` so the pure decision function
|
|
532
|
+
// sees a graph-backed value. When only the boolean is
|
|
533
|
+
// supplied, we treat it as the test seam (the CLI gates it
|
|
534
|
+
// behind `PEAKS_TEST_SEAM === '1'`).
|
|
535
|
+
inFlightBatch: input.probeInflightBatch !== undefined
|
|
536
|
+
? { hasInFlightBatch: input.probeInflightBatch() }
|
|
537
|
+
: input.inFlightBatch,
|
|
518
538
|
force: input.force,
|
|
519
539
|
bypassRedLine: input.bypassRedLine,
|
|
520
540
|
mode,
|
|
@@ -123,24 +123,6 @@ function renderContextBlock(probe) {
|
|
|
123
123
|
if (probe !== null && probe !== undefined) {
|
|
124
124
|
const usedPct = (probe.ratio * 100).toFixed(1);
|
|
125
125
|
const freePct = ((1 - probe.ratio) * 100).toFixed(1);
|
|
126
|
-
// Slice 4.0.7-dogfood-PR-11 (ice-cola surface probe 2026-08-02):
|
|
127
|
-
// when the source is conservative-fallback, the ratio is
|
|
128
|
-
// hard-coded to 0 (no signal available). The pre-rid prompt
|
|
129
|
-
// rendered "0.0% used (100.0% free)" without flagging that
|
|
130
|
-
// this is a fallback, so the sub-agent thought it had 100%
|
|
131
|
-
// free and never re-checked. The fix short-circuits to the
|
|
132
|
-
// "no probe available" message when the source is not one of
|
|
133
|
-
// the trusted values emitted by peaks-loop's own readers.
|
|
134
|
-
const KNOWN_SOURCES = new Set(['token-counted', 'transcript-estimate', 'statusline']);
|
|
135
|
-
if (!KNOWN_SOURCES.has(probe.source)) {
|
|
136
|
-
return `## Context window (no probe available)
|
|
137
|
-
|
|
138
|
-
The orchestrator captured a probe with source: \`${probe.source}\` (ratio ${usedPct}%) but the dispatch layer does not trust this source. The pre-rid dispatch prompt rendered this as "0.0% used (100.0% free)" without flagging the fallback, which caused sub-agents to think they had 100% free and never re-check. **Treat the displayed ratio as unverified.**
|
|
139
|
-
|
|
140
|
-
If you need to evaluate context pressure, run \`peaks code context-now --project <root>\` and trust its \`ratio\` field. Do not estimate from message length.
|
|
141
|
-
|
|
142
|
-
`;
|
|
143
|
-
}
|
|
144
126
|
const action = probe.ratio >= 0.95
|
|
145
127
|
? 'RED-LINE — call `peaks compact auto --execute` immediately.'
|
|
146
128
|
: probe.ratio >= 0.85
|
|
@@ -43,7 +43,7 @@ export interface DispatchRecord {
|
|
|
43
43
|
* truth for "this record is v3.1-form"; readers check
|
|
44
44
|
* `version === '3.1'` for forward-compatible dispatching.
|
|
45
45
|
*/
|
|
46
|
-
readonly version: '
|
|
46
|
+
readonly version: '4.0.0';
|
|
47
47
|
readonly createdAt: string;
|
|
48
48
|
readonly completedAt: string | null;
|
|
49
49
|
readonly outcome: DispatchOutcome;
|
|
@@ -120,6 +120,22 @@ export interface DispatchRecord {
|
|
|
120
120
|
* merge transcript.
|
|
121
121
|
*/
|
|
122
122
|
readonly mergeBackAttempts: number;
|
|
123
|
+
/**
|
|
124
|
+
* Slice 4.0.8 RD §4 D4c (presence-lease-graph): the workflow id +
|
|
125
|
+
* graph node id + graphRef this dispatch is bound to. Persisted
|
|
126
|
+
* directly in the dispatch record (NOT in a sidecar) so the
|
|
127
|
+
* envelope-writer `markCompleted` can auto-transition the bound
|
|
128
|
+
* graph node to `envelope-received` with `ackStatus=pending` in
|
|
129
|
+
* one protected update.
|
|
130
|
+
*
|
|
131
|
+
* The schema bump from `3.2 → 4.0.0` is BREAKING in the sense
|
|
132
|
+
* that the literal type is narrowed. The optional `?` keeps back-
|
|
133
|
+
* compat for old records that pre-date the binding (the
|
|
134
|
+
* `upgradeRecord` reader defaults them to `null`).
|
|
135
|
+
*/
|
|
136
|
+
readonly workflowId: string | null;
|
|
137
|
+
readonly graphNodeId: string | null;
|
|
138
|
+
readonly graphRef: string | null;
|
|
123
139
|
}
|
|
124
140
|
/** Input for the initial write. */
|
|
125
141
|
export type WriteInitialDispatchInput = {
|
|
@@ -148,6 +164,14 @@ export type WriteInitialDispatchInput = {
|
|
|
148
164
|
* time when `--isolation` is requested.
|
|
149
165
|
*/
|
|
150
166
|
isolationStartedAt?: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Slice 4.0.8: workflow graph binding for the dispatch. Defaults
|
|
169
|
+
* to `null` so a non-graph dispatch (legacy CLI flow, ad-hoc
|
|
170
|
+
* dispatch) still writes a v4.0.0 record.
|
|
171
|
+
*/
|
|
172
|
+
workflowId?: string | null;
|
|
173
|
+
graphNodeId?: string | null;
|
|
174
|
+
graphRef?: string | null;
|
|
151
175
|
};
|
|
152
176
|
/** Heartbeat write input. */
|
|
153
177
|
export type AppendHeartbeatInput = {
|
|
@@ -44,7 +44,7 @@ export function writeInitialDispatchRecord(input) {
|
|
|
44
44
|
const path = dispatchRecordPath(projectRoot, sessionId, requestId, now());
|
|
45
45
|
const safePath = assertSafeDispatchRecordPath(path, projectRoot);
|
|
46
46
|
const record = {
|
|
47
|
-
version: '
|
|
47
|
+
version: '4.0.0',
|
|
48
48
|
createdAt: now().toISOString(),
|
|
49
49
|
completedAt: null,
|
|
50
50
|
outcome: 'no-execution',
|
|
@@ -91,7 +91,14 @@ export function writeInitialDispatchRecord(input) {
|
|
|
91
91
|
// New records start with empty serviceKill and zero attempts;
|
|
92
92
|
// the merge-back-runner (Task 9) populates them in place.
|
|
93
93
|
serviceKill: [],
|
|
94
|
-
mergeBackAttempts: 0
|
|
94
|
+
mergeBackAttempts: 0,
|
|
95
|
+
// Slice 4.0.8: workflow graph binding (RD §4 D4c). Defaults to
|
|
96
|
+
// `null` for legacy / ad-hoc dispatches that do not bind a graph
|
|
97
|
+
// node; v4.0.0 schema is structural (required field), so a `null`
|
|
98
|
+
// is the explicit "no binding" state.
|
|
99
|
+
workflowId: typeof input.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.workflowId) ? input.workflowId : null,
|
|
100
|
+
graphNodeId: typeof input.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.graphNodeId) ? input.graphNodeId : null,
|
|
101
|
+
graphRef: typeof input.graphRef === 'string' && input.graphRef.length > 0 ? input.graphRef : null,
|
|
95
102
|
};
|
|
96
103
|
writeAtomic(safePath, record);
|
|
97
104
|
// Slice 2026-06-23-audit-4th #A4: register the path in the
|
|
@@ -425,6 +432,68 @@ export function markCompleted(input) {
|
|
|
425
432
|
writeAtomic(input.recordPath, next);
|
|
426
433
|
return { record: next };
|
|
427
434
|
});
|
|
435
|
+
// Slice 4.0.8 RD §4 D4c: after validating the artifact/envelope
|
|
436
|
+
// association with `dispatchRef`, automatically transition the
|
|
437
|
+
// bound graph node to `envelope-received` with
|
|
438
|
+
// `ackStatus=pending`. This is the auto-transition the parent
|
|
439
|
+
// ack protocol relies on — the canonical dispatch chain is
|
|
440
|
+
// `prepare -> dispatched -> running -> envelope-received -> consumed-by-parent`.
|
|
441
|
+
// The transition runs through `workflow-node-lifecycle.writeEnvelope`
|
|
442
|
+
// so the same typed error contract (PEAKS_ENVELOPE_GRAPH_MISMATCH,
|
|
443
|
+
// PEAKS_GRAPH_REF_BROKEN, etc.) is reused. Failures are swallowed
|
|
444
|
+
// (best-effort; the dispatch record itself is the source of truth
|
|
445
|
+
// and the transition is observable through the graph store).
|
|
446
|
+
if (result.record.workflowId !== null && result.record.graphNodeId !== null && result.record.graphRef !== null) {
|
|
447
|
+
try {
|
|
448
|
+
// Lazy ESM dynamic import: the graph lifecycle service is not
|
|
449
|
+
// on the dispatch hot path; it would be wasteful to import it
|
|
450
|
+
// for non-graph-bound dispatches. ESM dynamic import returns
|
|
451
|
+
// a promise; we do not await (best-effort) — the dispatch
|
|
452
|
+
// record write is the load-bearing artifact, and the graph
|
|
453
|
+
// node transition is observable to the next CLI call.
|
|
454
|
+
void (async () => {
|
|
455
|
+
try {
|
|
456
|
+
const lifecycleMod = await import('../workflow/workflow-node-lifecycle.js');
|
|
457
|
+
const storeMod = await import('../workflow/workflow-graph-store.js');
|
|
458
|
+
const sessionRoot = (() => {
|
|
459
|
+
try {
|
|
460
|
+
return storeMod.graphPathFor({
|
|
461
|
+
projectRoot: input.projectRoot ?? '',
|
|
462
|
+
sessionId: result.record.sessionId,
|
|
463
|
+
graphRef: result.record.graphRef ?? '',
|
|
464
|
+
workflowId: result.record.workflowId ?? '',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
if (sessionRoot === null)
|
|
472
|
+
return;
|
|
473
|
+
const graph = storeMod.readGraph({
|
|
474
|
+
projectRoot: input.projectRoot ?? '',
|
|
475
|
+
sessionId: result.record.sessionId,
|
|
476
|
+
graphRef: result.record.graphRef ?? '',
|
|
477
|
+
workflowId: result.record.workflowId ?? '',
|
|
478
|
+
});
|
|
479
|
+
const node = graph.nodes.find((n) => n.id === result.record.graphNodeId);
|
|
480
|
+
if (node === undefined)
|
|
481
|
+
return;
|
|
482
|
+
// Use the dispatch record's path as the dispatchRef. The
|
|
483
|
+
// record itself is the load-bearing artifact; the
|
|
484
|
+
// graph-node transition is a derived side-effect.
|
|
485
|
+
const dispatchRef = input.recordPath;
|
|
486
|
+
lifecycleMod.writeEnvelope({
|
|
487
|
+
graphNode: node,
|
|
488
|
+
dispatchRef,
|
|
489
|
+
envelopeDispatchRef: dispatchRef,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch { /* best-effort graph transition */ }
|
|
493
|
+
})();
|
|
494
|
+
}
|
|
495
|
+
catch { /* best-effort */ }
|
|
496
|
+
}
|
|
428
497
|
// Slice 2026-06-23-audit-4th #A4: update the active-dispatches
|
|
429
498
|
// index. Best-effort (the on-disk record is the source of truth);
|
|
430
499
|
// we only attempt the update when the trusted projectRoot is
|
|
@@ -549,18 +618,13 @@ function upgradeRecord(parsed) {
|
|
|
549
618
|
throw new Error('Dispatch record root must be an object');
|
|
550
619
|
}
|
|
551
620
|
const obj = parsed;
|
|
552
|
-
// Slice
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
// upgraded to v3.1 on read. The forward-compat check rejects
|
|
556
|
-
// records with an unknown version string (which would
|
|
557
|
-
// indicate either a much-older build or a much-newer one
|
|
558
|
-
// with a schema we have not implemented). v1 records
|
|
559
|
-
// (pre-audit-trail) must be regenerated.
|
|
621
|
+
// Slice 4.0.8: 3.2 → 4.0.0 schema bump. The literal type narrows
|
|
622
|
+
// to '4.0.0' but legacy v3.2 / v3.1 / 3 / 2 / 1 records are
|
|
623
|
+
// accepted transparently and upgraded on read.
|
|
560
624
|
const rawVersion = obj.version;
|
|
561
|
-
if (rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
|
|
562
|
-
throw new Error(`Dispatch record version mismatch: expected '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
|
|
563
|
-
'The v1 →
|
|
625
|
+
if (rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
|
|
626
|
+
throw new Error(`Dispatch record version mismatch: expected '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
|
|
627
|
+
'The v1 → v4.0.0 migration is in-file; records from much older or newer builds must be regenerated.');
|
|
564
628
|
}
|
|
565
629
|
const role = stringField(obj, 'role');
|
|
566
630
|
const requestId = stringField(obj, 'requestId');
|
|
@@ -602,7 +666,7 @@ function upgradeRecord(parsed) {
|
|
|
602
666
|
? obj.batchId
|
|
603
667
|
: 'legacy-batch';
|
|
604
668
|
return {
|
|
605
|
-
version: '
|
|
669
|
+
version: '4.0.0',
|
|
606
670
|
createdAt,
|
|
607
671
|
completedAt,
|
|
608
672
|
outcome,
|
|
@@ -652,7 +716,13 @@ function upgradeRecord(parsed) {
|
|
|
652
716
|
: [],
|
|
653
717
|
mergeBackAttempts: typeof obj.mergeBackAttempts === 'number' && Number.isFinite(obj.mergeBackAttempts) && obj.mergeBackAttempts >= 0
|
|
654
718
|
? Math.floor(obj.mergeBackAttempts)
|
|
655
|
-
: 0
|
|
719
|
+
: 0,
|
|
720
|
+
// Slice 4.0.8: 3.2 → 4.0.0 migration. v3.2 records on disk
|
|
721
|
+
// pre-date the workflow-graph binding; default all three
|
|
722
|
+
// fields to `null` so a legacy record upgrades transparently.
|
|
723
|
+
workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
|
|
724
|
+
graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
|
|
725
|
+
graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null
|
|
656
726
|
};
|
|
657
727
|
}
|
|
658
728
|
function isObject(v) {
|
|
@@ -34,20 +34,6 @@ export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandat
|
|
|
34
34
|
* (e.g. projectRoot for context) without churning every call site.
|
|
35
35
|
*/
|
|
36
36
|
export declare function formatTestToolDetection(): string;
|
|
37
|
-
/**
|
|
38
|
-
* Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
|
|
39
|
-
* `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
|
|
40
|
-
* template did not mention monorepos at all, so sub-agents on a
|
|
41
|
-
* monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
|
|
42
|
-
* fanned out to every sub-package — a 36-minute wall clock for what
|
|
43
|
-
* was a single-slice change. The block tells the sub-agent to scope
|
|
44
|
-
* to a single sub-package via `pnpm -r --filter @scope/name test`
|
|
45
|
-
* and to detect the layout via `peaks test --json` first.
|
|
46
|
-
*
|
|
47
|
-
* The block is empty when the project is not a monorepo, so the
|
|
48
|
-
* single-package case is byte-identical to the pre-rid prompt.
|
|
49
|
-
*/
|
|
50
|
-
export declare const MONOREPO_TEST_TOOL_BLOCK = "## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)\n\nWhen `<projectRoot>/pnpm-workspace.yaml` is present, the project is a pnpm monorepo. The root `package.json#scripts.test` is typically `pnpm -r test` (recursive across all packages) and `pnpm test` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:\n\n- detect the active sub-package by looking for `packages/<name>/package.json` (or via `pnpm -r --filter ${YOUR_PACKAGE_NAME} test`)\n- run `pnpm --filter @<scope>/<name> test <pattern>` for one sub-package only\n- if a top-level test script is genuinely needed across multiple packages, prefix with `PEAKS_FULL_TEST=1` AND justify in the final report\n\nA repo without `pnpm-workspace.yaml` has no monorepo block; the pre-rid static template applies unchanged.";
|
|
51
37
|
/**
|
|
52
38
|
* Slice 2026-07-29-dispatch-stall-governance / S5 (AC-4.1 / AC-4.2) —
|
|
53
39
|
* pure scope classifier. Returns a typed result so callers (the
|
|
@@ -54,28 +54,6 @@ If unsure which framework the consumer project uses, run \`peaks test --json\` f
|
|
|
54
54
|
export function formatTestToolDetection() {
|
|
55
55
|
return TEST_TOOL_DETECTION_BLOCK;
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
|
|
59
|
-
* `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
|
|
60
|
-
* template did not mention monorepos at all, so sub-agents on a
|
|
61
|
-
* monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
|
|
62
|
-
* fanned out to every sub-package — a 36-minute wall clock for what
|
|
63
|
-
* was a single-slice change. The block tells the sub-agent to scope
|
|
64
|
-
* to a single sub-package via `pnpm -r --filter @scope/name test`
|
|
65
|
-
* and to detect the layout via `peaks test --json` first.
|
|
66
|
-
*
|
|
67
|
-
* The block is empty when the project is not a monorepo, so the
|
|
68
|
-
* single-package case is byte-identical to the pre-rid prompt.
|
|
69
|
-
*/
|
|
70
|
-
export const MONOREPO_TEST_TOOL_BLOCK = `## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)
|
|
71
|
-
|
|
72
|
-
When \`<projectRoot>/pnpm-workspace.yaml\` is present, the project is a pnpm monorepo. The root \`package.json#scripts.test\` is typically \`pnpm -r test\` (recursive across all packages) and \`pnpm test\` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:
|
|
73
|
-
|
|
74
|
-
- detect the active sub-package by looking for \`packages/<name>/package.json\` (or via \`pnpm -r --filter \${YOUR_PACKAGE_NAME} test\`)
|
|
75
|
-
- run \`pnpm --filter @<scope>/<name> test <pattern>\` for one sub-package only
|
|
76
|
-
- if a top-level test script is genuinely needed across multiple packages, prefix with \`PEAKS_FULL_TEST=1\` AND justify in the final report
|
|
77
|
-
|
|
78
|
-
A repo without \`pnpm-workspace.yaml\` has no monorepo block; the pre-rid static template applies unchanged.`;
|
|
79
57
|
export function classifyTestCommand(command, env = process.env) {
|
|
80
58
|
if (typeof command !== 'string') {
|
|
81
59
|
return {
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Check: skill presence current + freshness
|
|
3
3
|
* (`skill-presence:current` and `skill-presence:freshness`).
|
|
4
4
|
*
|
|
5
|
+
* Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
|
|
6
|
+
* stale / lost / broken-graph leases by running a `dry-run` GC
|
|
7
|
+
* summary through `gcStalePresenceLeases`. The summary carries
|
|
8
|
+
* `removed` / `retained` counts plus per-lease warnings (typed
|
|
9
|
+
* `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
|
|
10
|
+
* surfaces them so the operator can run `peaks workspace reconcile`
|
|
11
|
+
* to repair.
|
|
12
|
+
*
|
|
5
13
|
* Two checks sharing one read of the `presence` context field.
|
|
6
14
|
* - `skill-presence:current` is informational: passes when the
|
|
7
15
|
* presence is wired, with a message describing the active
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Check: skill presence current + freshness
|
|
3
3
|
* (`skill-presence:current` and `skill-presence:freshness`).
|
|
4
4
|
*
|
|
5
|
+
* Slice 4.0.8 (RD §3 + §4 D3): the diagnostic also surfaces
|
|
6
|
+
* stale / lost / broken-graph leases by running a `dry-run` GC
|
|
7
|
+
* summary through `gcStalePresenceLeases`. The summary carries
|
|
8
|
+
* `removed` / `retained` counts plus per-lease warnings (typed
|
|
9
|
+
* `PEAKS_GRAPH_REF_BROKEN`, `PEAKS_GRAPH_CORRUPTED`); the doctor
|
|
10
|
+
* surfaces them so the operator can run `peaks workspace reconcile`
|
|
11
|
+
* to repair.
|
|
12
|
+
*
|
|
5
13
|
* Two checks sharing one read of the `presence` context field.
|
|
6
14
|
* - `skill-presence:current` is informational: passes when the
|
|
7
15
|
* presence is wired, with a message describing the active
|
|
@@ -56,7 +64,7 @@ function run({ options, presence }) {
|
|
|
56
64
|
checks.push({
|
|
57
65
|
id: 'skill-presence:freshness',
|
|
58
66
|
ok: false,
|
|
59
|
-
message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended`
|
|
67
|
+
message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended, or \`peaks skill lease gc\` to drain the canonical lease`
|
|
60
68
|
});
|
|
61
69
|
}
|
|
62
70
|
else {
|
|
@@ -12,5 +12,11 @@ export type PresenceMarkerWarning = (typeof PRESENCE_MARKER_WARNING)[number];
|
|
|
12
12
|
export declare const PRESENCE_MARKER_WARNING: readonly ["Peaks skill context may have been lost from this conversation; please re-invoke /peaks-<skill>."];
|
|
13
13
|
/**
|
|
14
14
|
* Pure read-only presence-marker detection. No I/O side effects.
|
|
15
|
+
*
|
|
16
|
+
* Slice 4.0.8: the canonical 4.0.7 behavior is preserved (read legacy
|
|
17
|
+
* `active-skill.json` or `.peaks/.active-skill.json`, project the
|
|
18
|
+
* `Peaks-Loop Skill:` marker). The new diagnostic for a broken
|
|
19
|
+
* canonical graph is surfaced via the `warning` field, NOT swallowed,
|
|
20
|
+
* so the statusline / hook / doctor consumers can render it.
|
|
15
21
|
*/
|
|
16
22
|
export declare function detectPresenceMarker(input: DetectPresenceMarkerInput): DetectPresenceMarkerResult;
|