peaks-cli 1.3.1 → 1.3.3
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/README.md +6 -2
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/slice-commands.js +4 -2
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workspace-commands.js +70 -14
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
- package/dist/src/services/artifacts/request-artifact-service.js +116 -76
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/doctor/doctor-service.d.ts +62 -0
- package/dist/src/services/doctor/doctor-service.js +276 -1
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +120 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +137 -28
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/slice/slice-check-service.js +20 -1
- package/dist/src/services/slice/slice-check-types.d.ts +9 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/workspace/migrate-service.js +124 -2
- package/dist/src/services/workspace/migrate-types.d.ts +50 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
- package/dist/src/services/workspace/reconcile-service.js +267 -48
- package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
- package/dist/src/services/workspace/workspace-service.js +29 -62
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +58 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +52 -9
- package/skills/peaks-solo/SKILL.md +83 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +19 -0
- package/skills/peaks-ui/SKILL.md +28 -1
|
@@ -3,8 +3,8 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { isDirectory, listDirectories } from '../../shared/fs.js';
|
|
5
5
|
import { checkPrerequisites, DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES } from './artifact-prerequisites.js';
|
|
6
|
-
import { ensureSession } from '../session/session-manager.js';
|
|
7
|
-
import { getCurrentChangeId
|
|
6
|
+
import { ensureSession, getSessionIdCanonical } from '../session/session-manager.js';
|
|
7
|
+
import { getCurrentChangeId } from '../../shared/change-id.js';
|
|
8
8
|
import { getNextNumber, buildNumberedFilename } from '../../shared/incrementing-number.js';
|
|
9
9
|
import { lintRequestArtifact } from './artifact-lint-service.js';
|
|
10
10
|
import { checkTypeSanity } from '../scan/type-sanity-service.js';
|
|
@@ -313,25 +313,48 @@ export async function createRequestArtifact(options) {
|
|
|
313
313
|
const clock = options.clock ?? defaultClock;
|
|
314
314
|
const timestamp = clock();
|
|
315
315
|
// Use provided session ID or get/create current session. The session
|
|
316
|
-
// id is
|
|
317
|
-
// which session wrote it), but the artifact file is now written
|
|
318
|
-
// under the change-id dir, NOT the session dir.
|
|
316
|
+
// id is the binding for the artifact file's location.
|
|
319
317
|
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
318
|
+
// Slice 006 collapses the per-change-id top-level dirs. The artifact
|
|
319
|
+
// file is now written under the SESSION dir
|
|
320
|
+
// (`.peaks/_runtime/<sid>/<role>/requests/`) instead of the
|
|
321
|
+
// change-id dir. The 2-tier fallback (canonical session → legacy
|
|
322
|
+
// session) replaces the F3 3-tier fallback (per-change-id →
|
|
323
|
+
// canonical session → legacy session). The change-id is preserved
|
|
324
|
+
// in the artifact body's frontmatter (under `- change-id:`) for
|
|
325
|
+
// human navigation; it is no longer a filesystem path key.
|
|
326
326
|
const sessionId = options.sessionId ?? await ensureSession(options.projectRoot);
|
|
327
327
|
const boundChangeId = getCurrentChangeId(options.projectRoot);
|
|
328
|
-
// Resolution order for the change-id (file
|
|
329
|
-
// 1. Explicit `options.changeId` (CLI `--
|
|
328
|
+
// Resolution order for the change-id (file body metadata):
|
|
329
|
+
// 1. Explicit `options.changeId` (CLI `--change-id`).
|
|
330
330
|
// 2. `current-change` binding (live developer working context).
|
|
331
331
|
// 3. The requestId itself (every request is its own scope by default).
|
|
332
332
|
const changeId = options.changeId ?? boundChangeId ?? options.requestId;
|
|
333
|
-
//
|
|
334
|
-
|
|
333
|
+
// Slice 008 (F21 fix): fail fast when the resolved session id
|
|
334
|
+
// looks like a real session id (matches the date+session prefix)
|
|
335
|
+
// but does NOT correspond to an actual session dir under
|
|
336
|
+
// `.peaks/_runtime/`. Pre-F21 a sub-agent with a typo or stale
|
|
337
|
+
// binding (e.g. `2025-01-01-session-deadbe`) silently planned
|
|
338
|
+
// to write to a non-existent path. The check is intentionally
|
|
339
|
+
// scoped to "looks like a real session id" — a sid like
|
|
340
|
+
// `test-session` or `s` (no date prefix) is allowed through so
|
|
341
|
+
// the existing F3 / slice-007 back-compat flows (e.g. the
|
|
342
|
+
// `peaks request init --session-id <arbitrary-scope>` tests)
|
|
343
|
+
// can still create the dir on demand via the writer's
|
|
344
|
+
// `mkdir(..., { recursive: true })`.
|
|
345
|
+
const LOOKS_LIKE_SESSION_ID = /^\d{4}-\d{2}-\d{2}-session-/;
|
|
346
|
+
if (LOOKS_LIKE_SESSION_ID.test(sessionId)) {
|
|
347
|
+
const sessionDir = join(options.projectRoot, '.peaks', '_runtime', sessionId);
|
|
348
|
+
if (!(await isDirectory(sessionDir))) {
|
|
349
|
+
const canonicalSid = getSessionIdCanonical(options.projectRoot);
|
|
350
|
+
const hint = canonicalSid !== null
|
|
351
|
+
? `Use --session-id ${canonicalSid} or run 'peaks workspace init' to create a new session.`
|
|
352
|
+
: `Run 'peaks workspace init' to create a new session.`;
|
|
353
|
+
throw new Error(`session id '${sessionId}' does not exist in _runtime/. Current canonical binding is '${canonicalSid ?? '<none>'}'. ${hint}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Build numbered path under the session dir (canonical post-F3 home).
|
|
357
|
+
const requestsDir = join(options.projectRoot, '.peaks', '_runtime', sessionId, options.role, 'requests');
|
|
335
358
|
// Check if a file with this requestId already exists (regardless of number prefix)
|
|
336
359
|
if (await isDirectory(requestsDir)) {
|
|
337
360
|
const existingFiles = await listMarkdownFiles(requestsDir);
|
|
@@ -356,11 +379,11 @@ export async function createRequestArtifact(options) {
|
|
|
356
379
|
await mkdir(dirname(path), { recursive: true });
|
|
357
380
|
await writeFile(path, content, 'utf8');
|
|
358
381
|
// Create QA initiated marker so rd:qa-handoff gate can verify QA was invoked.
|
|
359
|
-
//
|
|
360
|
-
// the change-id dir
|
|
361
|
-
//
|
|
382
|
+
// Slice 006: the marker lives under the SESSION dir (canonical post-F3
|
|
383
|
+
// home), not the change-id dir. The gate's prereq scan finds it at
|
|
384
|
+
// `.peaks/_runtime/<sid>/qa/.initiated`.
|
|
362
385
|
if (options.role === 'qa') {
|
|
363
|
-
const qaDir =
|
|
386
|
+
const qaDir = join(options.projectRoot, '.peaks', '_runtime', sessionId, 'qa');
|
|
364
387
|
const initiatedPath = join(qaDir, '.initiated');
|
|
365
388
|
if (!existsSync(initiatedPath)) {
|
|
366
389
|
await mkdir(qaDir, { recursive: true });
|
|
@@ -448,45 +471,45 @@ export async function listRequestArtifacts(options) {
|
|
|
448
471
|
if (!(await isDirectory(peaksRoot))) {
|
|
449
472
|
return [];
|
|
450
473
|
}
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
474
|
+
// Slice 006 collapsed the per-change-id top-level dirs. The 2-tier
|
|
475
|
+
// resolution model is:
|
|
476
|
+
// 1. `.peaks/_runtime/<sid>/<role>/requests/` (post-F3 canonical
|
|
477
|
+
// session home; slice 006's primary home for request artifacts).
|
|
478
|
+
// 2. `.peaks/<sid>/<role>/requests/` (pre-F3 legacy home; back-compat
|
|
479
|
+
// for users who have not yet migrated).
|
|
456
480
|
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// (
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
// Skip well-known non-artifact dirs: `_runtime/` holds ephemeral state
|
|
467
|
-
// (no `requests/` subdirs anyway, but skip explicitly to avoid noise).
|
|
468
|
-
const allDirs = await listDirectories(peaksRoot);
|
|
469
|
-
const candidateDirs = allDirs.filter((dir) => dir !== '_runtime');
|
|
470
|
-
// Expand scopes to include the nested umbrellas that host change-id dirs
|
|
471
|
-
// (retrospective/, _dogfood/). For each, list its sub-dirs and treat
|
|
472
|
-
// them as additional scopes. This makes the lookup span the entire
|
|
473
|
-
// .peaks tree.
|
|
474
|
-
const expandedScopes = [];
|
|
481
|
+
// When `sessionId` is pinned, the function scans that one session's
|
|
482
|
+
// two tiers (canonical + legacy). When `sessionId` is NOT pinned,
|
|
483
|
+
// the function scans every session dir under `.peaks/_runtime/`
|
|
484
|
+
// (canonical) AND every legacy session dir under `.peaks/`
|
|
485
|
+
// (top-level). Per-change-id dirs (the old `.peaks/<changeId>/<role>/`
|
|
486
|
+
// layout) are NOT scanned — slice 008 will migrate the 5
|
|
487
|
+
// already-shipped slices' artifacts to the new layout; new request
|
|
488
|
+
// artifacts are written to the session dir directly.
|
|
489
|
+
const scopes = [];
|
|
475
490
|
if (options.sessionId !== undefined) {
|
|
476
|
-
|
|
491
|
+
scopes.push(join('_runtime', options.sessionId));
|
|
492
|
+
scopes.push(options.sessionId);
|
|
477
493
|
}
|
|
478
494
|
else {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
for (const n of nested) {
|
|
484
|
-
expandedScopes.push(join(dir, n));
|
|
485
|
-
}
|
|
495
|
+
const runtimeRoot = join(peaksRoot, '_runtime');
|
|
496
|
+
if (await isDirectory(runtimeRoot)) {
|
|
497
|
+
for (const sid of await listDirectories(runtimeRoot)) {
|
|
498
|
+
scopes.push(join('_runtime', sid));
|
|
486
499
|
}
|
|
487
500
|
}
|
|
501
|
+
// Legacy top-level session dirs: scan every non-`._peaks` top-level
|
|
502
|
+
// dir as a potential legacy scope. Slice 006 dropped per-change-id
|
|
503
|
+
// dirs, so any top-level dir name under `.peaks/` that is NOT
|
|
504
|
+
// `_runtime` (and not a well-known umbrella like retrospective,
|
|
505
|
+
// _dogfood, memory, etc. — those have no `<role>/requests/` tree)
|
|
506
|
+
// is treated as a candidate legacy session dir.
|
|
507
|
+
for (const dir of await listDirectories(peaksRoot)) {
|
|
508
|
+
if (dir === '_runtime')
|
|
509
|
+
continue;
|
|
510
|
+
scopes.push(dir);
|
|
511
|
+
}
|
|
488
512
|
}
|
|
489
|
-
const scopes = expandedScopes;
|
|
490
513
|
const roles = options.role !== undefined ? [options.role] : Array.from(VALID_ROLES);
|
|
491
514
|
const summaries = [];
|
|
492
515
|
for (const scope of scopes) {
|
|
@@ -525,45 +548,56 @@ export async function showRequestArtifact(options) {
|
|
|
525
548
|
// As of slice 2026-06-05-change-id-as-unit-of-work, the dir key is the
|
|
526
549
|
// change-id (not the session-id). When the caller pins `sessionId` we
|
|
527
550
|
// use it as the scope anyway (legacy callers, and tests that pass
|
|
528
|
-
// `STABLE_SESSION` as a stand-in).
|
|
529
|
-
//
|
|
530
|
-
//
|
|
551
|
+
// `STABLE_SESSION` as a stand-in).
|
|
552
|
+
//
|
|
553
|
+
// As of slice 2026-06-06-session-layout-canonicalize (F3), the
|
|
554
|
+
// canonical home for session dirs is `.peaks/_runtime/<sid>/`.
|
|
555
|
+
// The pre-F3 layout `.peaks/<sid>/` is preserved as a one-minor
|
|
556
|
+
// back-compat fallback (the new path wins when both exist). We
|
|
557
|
+
// resolve the dir to use UP FRONT (not lazily after a miss) so the
|
|
558
|
+
// prerequisite gate's "request artifact present" check observes
|
|
559
|
+
// the same path the rest of the canonical layout uses.
|
|
531
560
|
if (options.sessionId !== undefined) {
|
|
532
|
-
const
|
|
561
|
+
const canonicalDir = join(options.projectRoot, '.peaks', '_runtime', options.sessionId, options.role, 'requests');
|
|
562
|
+
const legacyDir = join(options.projectRoot, '.peaks', options.sessionId, options.role, 'requests');
|
|
563
|
+
// Try the canonical (post-F3) path first; fall back to the legacy
|
|
564
|
+
// path only if the canonical path is absent. The legacy path is
|
|
565
|
+
// expected to be empty after a `peaks workspace migrate --to-runtime`
|
|
566
|
+
// run; this fallback exists for users who have not yet migrated.
|
|
567
|
+
const dir = (await isDirectory(canonicalDir)) ? canonicalDir : legacyDir;
|
|
568
|
+
const scope = dir === canonicalDir
|
|
569
|
+
? join('_runtime', options.sessionId)
|
|
570
|
+
: options.sessionId;
|
|
533
571
|
const found = await findFileInDir(dir);
|
|
534
572
|
if (found === null) {
|
|
535
573
|
return null;
|
|
536
574
|
}
|
|
537
|
-
return await readRequestArtifact(options.projectRoot,
|
|
575
|
+
return await readRequestArtifact(options.projectRoot, scope, options.role, found);
|
|
538
576
|
}
|
|
539
577
|
const peaksRoot = join(options.projectRoot, '.peaks');
|
|
540
578
|
if (!(await isDirectory(peaksRoot))) {
|
|
541
579
|
return null;
|
|
542
580
|
}
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
continue;
|
|
554
|
-
scopes.push(dir);
|
|
555
|
-
if (dir === 'retrospective' || dir === '_dogfood') {
|
|
556
|
-
const nested = await listDirectories(join(peaksRoot, dir));
|
|
557
|
-
for (const n of nested) {
|
|
558
|
-
scopes.push(join(dir, n));
|
|
581
|
+
// Slice 006: scan only session-scoped dirs (canonical + legacy)
|
|
582
|
+
// for the artifact. The per-change-id top-level dirs are no longer
|
|
583
|
+
// scanned — they are frozen until slice 008 migrates them.
|
|
584
|
+
const runtimeRoot = join(peaksRoot, '_runtime');
|
|
585
|
+
if (await isDirectory(runtimeRoot)) {
|
|
586
|
+
for (const sid of await listDirectories(runtimeRoot)) {
|
|
587
|
+
const dir = join(runtimeRoot, sid, options.role, 'requests');
|
|
588
|
+
const found = await findFileInDir(dir);
|
|
589
|
+
if (found !== null) {
|
|
590
|
+
return await readRequestArtifact(options.projectRoot, join('_runtime', sid), options.role, found);
|
|
559
591
|
}
|
|
560
592
|
}
|
|
561
593
|
}
|
|
562
|
-
for (const
|
|
563
|
-
|
|
564
|
-
|
|
594
|
+
for (const dir of await listDirectories(peaksRoot)) {
|
|
595
|
+
if (dir === '_runtime')
|
|
596
|
+
continue;
|
|
597
|
+
const target = join(peaksRoot, dir, options.role, 'requests');
|
|
598
|
+
const found = await findFileInDir(target);
|
|
565
599
|
if (found !== null) {
|
|
566
|
-
return await readRequestArtifact(options.projectRoot,
|
|
600
|
+
return await readRequestArtifact(options.projectRoot, dir, options.role, found);
|
|
567
601
|
}
|
|
568
602
|
}
|
|
569
603
|
return null;
|
|
@@ -719,6 +753,12 @@ export async function transitionRequestArtifact(options) {
|
|
|
719
753
|
const prerequisiteResult = await checkPrerequisites({
|
|
720
754
|
projectRoot: options.projectRoot,
|
|
721
755
|
changeId: existing.changeId,
|
|
756
|
+
// F3 repair cycle 1: pass the session binding so the gate can fall
|
|
757
|
+
// back to `.peaks/_runtime/<sid>/<role>/` (and the legacy
|
|
758
|
+
// `.peaks/<sid>/<role>/`) for prerequisite artifacts that still
|
|
759
|
+
// live under the session dir rather than the change-id dir. This
|
|
760
|
+
// mirrors the F1/F2 back-compat pattern.
|
|
761
|
+
sessionId: existing.sessionId,
|
|
722
762
|
role: options.role,
|
|
723
763
|
newState: options.newState,
|
|
724
764
|
requestId: options.requestId,
|
|
@@ -61,7 +61,7 @@ export type PeaksConfig = {
|
|
|
61
61
|
/**
|
|
62
62
|
* Sub-agent progress surfacing knobs. The `peaks progress watch`
|
|
63
63
|
* CLI (intended to be run in a separate terminal tab while the
|
|
64
|
-
* LLM is working) reads `.peaks/<sid>/
|
|
64
|
+
* LLM is working) reads `.peaks/_sub_agents/<sid>/subagent-progress.json`
|
|
65
65
|
* and renders elapsed / spinner / sub-step in real time. The
|
|
66
66
|
* `enabled` flag is a kill-switch for users who find the watch
|
|
67
67
|
* distracting; the `heartbeatIntervalMs` lets power users tune
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type ArtifactStatus = 'created' | 'finalized' | 'partial' | 'failed';
|
|
2
|
+
export interface ArtifactMeta {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly size: number;
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
readonly status: ArtifactStatus;
|
|
7
|
+
/** Mandatory literal `false`. The type system rejects `true`. */
|
|
8
|
+
readonly contentInlined: false;
|
|
9
|
+
/** 1-2 sentence description, ≤ 200 chars. Allowed in main context. */
|
|
10
|
+
readonly summary: string | null;
|
|
11
|
+
/** ISO8601. */
|
|
12
|
+
readonly writtenAt: string;
|
|
13
|
+
/** Request id this artifact belongs to. */
|
|
14
|
+
readonly rid: string;
|
|
15
|
+
/** Sub-agent role string. */
|
|
16
|
+
readonly role: string;
|
|
17
|
+
/** Sequence number when same role is dispatched multiple times. */
|
|
18
|
+
readonly idx: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ContextImpact {
|
|
21
|
+
readonly promptSize: number;
|
|
22
|
+
readonly artifactSizes: readonly number[];
|
|
23
|
+
readonly batchTotalSize: number;
|
|
24
|
+
/** `high` if `batchTotalSize > 4MB` OR any `artifactSize > 1MB`. */
|
|
25
|
+
readonly contextWarning: 'normal' | 'high' | 'critical';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compute the sha256 hex digest of a file. Throws if the file does not
|
|
29
|
+
* exist or is not readable. Caller is expected to handle ENOENT as
|
|
30
|
+
* `code: 'ARTIFACT_NOT_FOUND'` and treat 0-byte files as `status: 'failed'`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeSha256(filePath: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Build an `ArtifactMeta` from on-disk file. Computes size + sha256.
|
|
35
|
+
*
|
|
36
|
+
* `status` semantics:
|
|
37
|
+
* - `'created'` — file exists, non-empty, sha256 succeeded
|
|
38
|
+
* - `'failed'` — file is 0 bytes (R-2 / G7.4.e: do not silently succeed)
|
|
39
|
+
* - `'partial'` — caller-provided (e.g. sub-agent reports unfinished work)
|
|
40
|
+
* - `'finalized'` — caller-provided (e.g. sub-agent reports complete)
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildArtifactMeta(opts: {
|
|
43
|
+
path: string;
|
|
44
|
+
rid: string;
|
|
45
|
+
role: string;
|
|
46
|
+
idx: number;
|
|
47
|
+
summary: string | null;
|
|
48
|
+
status?: ArtifactStatus;
|
|
49
|
+
/** Override sha256 / size (e.g. when caller already computed them). */
|
|
50
|
+
precomputed?: {
|
|
51
|
+
size: number;
|
|
52
|
+
sha256: string;
|
|
53
|
+
};
|
|
54
|
+
/** Override the writtenAt timestamp. */
|
|
55
|
+
writtenAt?: string;
|
|
56
|
+
}): ArtifactMeta;
|
|
57
|
+
/**
|
|
58
|
+
* Build a `ContextImpact` from a prompt size + artifact sizes.
|
|
59
|
+
* Computes `contextWarning` per the G7.3 rule:
|
|
60
|
+
* - `'critical'` if any artifact > ARTIFACT_MAX_SIZE_BYTES
|
|
61
|
+
* - `'high'` if total > BATCH_TOTAL_HIGH_BYTES (4MB)
|
|
62
|
+
* - `'normal'` otherwise
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildContextImpact(opts: {
|
|
65
|
+
promptSize: number;
|
|
66
|
+
artifactSizes: readonly number[];
|
|
67
|
+
}): ContextImpact;
|
|
68
|
+
export declare const ARTIFACT_LIMITS: {
|
|
69
|
+
readonly ARTIFACT_MAX_SIZE_BYTES: number;
|
|
70
|
+
readonly BATCH_TOTAL_HIGH_BYTES: number;
|
|
71
|
+
readonly ARTIFACT_SUMMARY_MAX_CHARS: 200;
|
|
72
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G7 — sub-agent context minimal-occupation (RL-17..RL-22, AC-38..AC-43).
|
|
3
|
+
*
|
|
4
|
+
* `ArtifactMeta` is what the dispatch record stores per sub-agent artifact
|
|
5
|
+
* instead of the full content. The `contentInlined: false` literal is the
|
|
6
|
+
* API contract: the type system rejects `true`, so main LLM context can
|
|
7
|
+
* never accidentally be flooded with inlined artifact bodies.
|
|
8
|
+
*
|
|
9
|
+
* The artifact's content lives on disk at `path`; the meta is ~200 chars
|
|
10
|
+
* (path + size + sha256 + status + summary), so 3 sub-agents × ~200 chars
|
|
11
|
+
* = ~600 chars net context increase per batch instead of 3MB+.
|
|
12
|
+
*
|
|
13
|
+
* Path convention (G7.4.c):
|
|
14
|
+
* `.peaks/_sub_agents/<sid>/artifacts/<rid>-<role>-<idx>.<ext>`
|
|
15
|
+
*
|
|
16
|
+
* See: `.peaks/memory/sub-agent-context-minimal-occupation.md` for the
|
|
17
|
+
* full G7 rule.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
21
|
+
const ARTIFACT_MAX_SIZE_BYTES = 1024 * 1024; // 1MB
|
|
22
|
+
const BATCH_TOTAL_HIGH_BYTES = 4 * 1024 * 1024; // 4MB
|
|
23
|
+
/**
|
|
24
|
+
* Compute the sha256 hex digest of a file. Throws if the file does not
|
|
25
|
+
* exist or is not readable. Caller is expected to handle ENOENT as
|
|
26
|
+
* `code: 'ARTIFACT_NOT_FOUND'` and treat 0-byte files as `status: 'failed'`.
|
|
27
|
+
*/
|
|
28
|
+
export function computeSha256(filePath) {
|
|
29
|
+
const content = readFileSync(filePath);
|
|
30
|
+
return createHash('sha256').update(content).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build an `ArtifactMeta` from on-disk file. Computes size + sha256.
|
|
34
|
+
*
|
|
35
|
+
* `status` semantics:
|
|
36
|
+
* - `'created'` — file exists, non-empty, sha256 succeeded
|
|
37
|
+
* - `'failed'` — file is 0 bytes (R-2 / G7.4.e: do not silently succeed)
|
|
38
|
+
* - `'partial'` — caller-provided (e.g. sub-agent reports unfinished work)
|
|
39
|
+
* - `'finalized'` — caller-provided (e.g. sub-agent reports complete)
|
|
40
|
+
*/
|
|
41
|
+
export function buildArtifactMeta(opts) {
|
|
42
|
+
let size;
|
|
43
|
+
let sha256;
|
|
44
|
+
let status = opts.status ?? 'created';
|
|
45
|
+
if (opts.precomputed) {
|
|
46
|
+
size = opts.precomputed.size;
|
|
47
|
+
sha256 = opts.precomputed.sha256;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const stat = statSync(opts.path);
|
|
51
|
+
size = stat.size;
|
|
52
|
+
if (size === 0) {
|
|
53
|
+
// 0-byte artifact: cannot compute meaningful sha256; mark as failed.
|
|
54
|
+
sha256 = '0'.repeat(64);
|
|
55
|
+
status = 'failed';
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
sha256 = computeSha256(opts.path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const summary = opts.summary;
|
|
62
|
+
if (summary !== null && summary.length > 200) {
|
|
63
|
+
throw new Error(`ArtifactMeta summary must be ≤ 200 chars (got ${summary.length})`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
path: opts.path,
|
|
67
|
+
size,
|
|
68
|
+
sha256,
|
|
69
|
+
status,
|
|
70
|
+
contentInlined: false,
|
|
71
|
+
summary,
|
|
72
|
+
writtenAt: opts.writtenAt ?? new Date().toISOString(),
|
|
73
|
+
rid: opts.rid,
|
|
74
|
+
role: opts.role,
|
|
75
|
+
idx: opts.idx
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build a `ContextImpact` from a prompt size + artifact sizes.
|
|
80
|
+
* Computes `contextWarning` per the G7.3 rule:
|
|
81
|
+
* - `'critical'` if any artifact > ARTIFACT_MAX_SIZE_BYTES
|
|
82
|
+
* - `'high'` if total > BATCH_TOTAL_HIGH_BYTES (4MB)
|
|
83
|
+
* - `'normal'` otherwise
|
|
84
|
+
*/
|
|
85
|
+
export function buildContextImpact(opts) {
|
|
86
|
+
const batchTotalSize = opts.promptSize + opts.artifactSizes.reduce((a, b) => a + b, 0);
|
|
87
|
+
let contextWarning = 'normal';
|
|
88
|
+
if (opts.artifactSizes.some((s) => s > ARTIFACT_MAX_SIZE_BYTES)) {
|
|
89
|
+
contextWarning = 'critical';
|
|
90
|
+
}
|
|
91
|
+
else if (batchTotalSize > BATCH_TOTAL_HIGH_BYTES) {
|
|
92
|
+
contextWarning = 'high';
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
promptSize: opts.promptSize,
|
|
96
|
+
artifactSizes: opts.artifactSizes,
|
|
97
|
+
batchTotalSize,
|
|
98
|
+
contextWarning
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export const ARTIFACT_LIMITS = {
|
|
102
|
+
ARTIFACT_MAX_SIZE_BYTES,
|
|
103
|
+
BATCH_TOTAL_HIGH_BYTES,
|
|
104
|
+
ARTIFACT_SUMMARY_MAX_CHARS: 200
|
|
105
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G9 — forced compression gate (RL-27..RL-32).
|
|
3
|
+
*
|
|
4
|
+
* The CLI 兜底 layer. Validates prompt size against the threshold
|
|
5
|
+
* table in `threshold.ts` and returns a decision. The PreToolUse hook
|
|
6
|
+
* layer (`peaks sub-agent-dispatch-guard`) re-runs the same logic
|
|
7
|
+
* without `--force` (RL-30 strict).
|
|
8
|
+
*
|
|
9
|
+
* Decision codes:
|
|
10
|
+
* - `OK` — under 50%
|
|
11
|
+
* - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
|
|
12
|
+
* - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
|
|
13
|
+
* - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
|
|
14
|
+
* - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
|
|
15
|
+
* - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
|
|
16
|
+
*
|
|
17
|
+
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
|
|
18
|
+
*/
|
|
19
|
+
import { tierToCode, type ThresholdEvaluation } from './threshold.js';
|
|
20
|
+
export type ContextGuardCode = 'OK' | 'CONTEXT_SOFT_WARN' | 'CONTEXT_NEAR_LIMIT' | 'PROMPT_TOO_LARGE' | 'PROMPT_EMERGENCY' | 'FORCED_OVER_THRESHOLD';
|
|
21
|
+
export interface ContextGuardDecision {
|
|
22
|
+
readonly allow: boolean;
|
|
23
|
+
readonly code: ContextGuardCode;
|
|
24
|
+
readonly warnings: readonly string[];
|
|
25
|
+
readonly suggest: string | null;
|
|
26
|
+
readonly evaluation: ThresholdEvaluation;
|
|
27
|
+
/** ISO8601 timestamp when --force override was applied. null otherwise. */
|
|
28
|
+
readonly forcedAt: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface ContextGuardOptions {
|
|
31
|
+
/** Pass `true` to allow override at the ≥ 80% tier. CLI-only; hook layer MUST NOT set this. */
|
|
32
|
+
readonly force?: boolean;
|
|
33
|
+
/** Override the default 256K context capacity (e.g. for tests). */
|
|
34
|
+
readonly capacityBytes?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate a prompt size against the G9.3 threshold table.
|
|
38
|
+
*
|
|
39
|
+
* The `force` option is the **only** path that lets a ≥ 80% prompt
|
|
40
|
+
* through. The hook layer (PreToolUse) MUST NOT accept this option;
|
|
41
|
+
* it is enforced by the `peaks sub-agent-dispatch-guard` atom's
|
|
42
|
+
* command-line parser, which does not declare a `--force` flag.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluatePromptSize(promptSize: number, opts?: ContextGuardOptions): ContextGuardDecision;
|
|
45
|
+
/**
|
|
46
|
+
* Re-export of `tierToCode` for callers that want a stable mapping
|
|
47
|
+
* without depending on the threshold module directly.
|
|
48
|
+
*/
|
|
49
|
+
export { tierToCode };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G9 — forced compression gate (RL-27..RL-32).
|
|
3
|
+
*
|
|
4
|
+
* The CLI 兜底 layer. Validates prompt size against the threshold
|
|
5
|
+
* table in `threshold.ts` and returns a decision. The PreToolUse hook
|
|
6
|
+
* layer (`peaks sub-agent-dispatch-guard`) re-runs the same logic
|
|
7
|
+
* without `--force` (RL-30 strict).
|
|
8
|
+
*
|
|
9
|
+
* Decision codes:
|
|
10
|
+
* - `OK` — under 50%
|
|
11
|
+
* - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
|
|
12
|
+
* - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
|
|
13
|
+
* - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
|
|
14
|
+
* - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
|
|
15
|
+
* - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
|
|
16
|
+
*
|
|
17
|
+
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
|
|
18
|
+
*/
|
|
19
|
+
import { CONTEXT_CAPACITY_DEFAULT_BYTES, evaluateThresholdTier, tierToCode } from './threshold.js';
|
|
20
|
+
const NEAR_LIMIT_SUGGEST = 'Consider --use-headroom to compress prompt.';
|
|
21
|
+
const SOFT_WARN_SUGGEST = 'Use --use-headroom to compress prompt proactively.';
|
|
22
|
+
const HARD_REJECT_SUGGEST = 'Trim prompt to < 80% of context capacity. Pass --force at CLI to override (NOT allowed at hook layer).';
|
|
23
|
+
const EMERGENCY_SUGGEST = 'Prompt exceeds 90% of context. Trim aggressively or split into multiple dispatches.';
|
|
24
|
+
/**
|
|
25
|
+
* Evaluate a prompt size against the G9.3 threshold table.
|
|
26
|
+
*
|
|
27
|
+
* The `force` option is the **only** path that lets a ≥ 80% prompt
|
|
28
|
+
* through. The hook layer (PreToolUse) MUST NOT accept this option;
|
|
29
|
+
* it is enforced by the `peaks sub-agent-dispatch-guard` atom's
|
|
30
|
+
* command-line parser, which does not declare a `--force` flag.
|
|
31
|
+
*/
|
|
32
|
+
export function evaluatePromptSize(promptSize, opts = {}) {
|
|
33
|
+
const capacity = opts.capacityBytes ?? CONTEXT_CAPACITY_DEFAULT_BYTES;
|
|
34
|
+
const evaluation = evaluateThresholdTier(promptSize, capacity);
|
|
35
|
+
const tier = evaluation.tier;
|
|
36
|
+
let allow;
|
|
37
|
+
let code;
|
|
38
|
+
let suggest;
|
|
39
|
+
let forcedAt = null;
|
|
40
|
+
switch (tier) {
|
|
41
|
+
case 'ok':
|
|
42
|
+
allow = true;
|
|
43
|
+
code = 'OK';
|
|
44
|
+
suggest = null;
|
|
45
|
+
break;
|
|
46
|
+
case 'soft-warn':
|
|
47
|
+
allow = true;
|
|
48
|
+
code = 'CONTEXT_SOFT_WARN';
|
|
49
|
+
suggest = SOFT_WARN_SUGGEST;
|
|
50
|
+
break;
|
|
51
|
+
case 'near-limit':
|
|
52
|
+
allow = true;
|
|
53
|
+
code = 'CONTEXT_NEAR_LIMIT';
|
|
54
|
+
suggest = NEAR_LIMIT_SUGGEST;
|
|
55
|
+
break;
|
|
56
|
+
case 'hard-reject':
|
|
57
|
+
allow = false;
|
|
58
|
+
code = 'PROMPT_TOO_LARGE';
|
|
59
|
+
suggest = HARD_REJECT_SUGGEST;
|
|
60
|
+
break;
|
|
61
|
+
case 'emergency':
|
|
62
|
+
allow = false;
|
|
63
|
+
code = 'PROMPT_EMERGENCY';
|
|
64
|
+
suggest = EMERGENCY_SUGGEST;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// --force override (CLI only; hook layer never reaches this branch)
|
|
68
|
+
if (!allow && opts.force === true) {
|
|
69
|
+
allow = true;
|
|
70
|
+
code = 'FORCED_OVER_THRESHOLD';
|
|
71
|
+
suggest = 'Override applied at CLI. Hook layer will still reject.';
|
|
72
|
+
forcedAt = new Date().toISOString();
|
|
73
|
+
}
|
|
74
|
+
const warnings = [...evaluation.warnings];
|
|
75
|
+
if (forcedAt !== null && !warnings.includes('FORCED_OVER_THRESHOLD')) {
|
|
76
|
+
warnings.push('FORCED_OVER_THRESHOLD');
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
allow,
|
|
80
|
+
code,
|
|
81
|
+
warnings,
|
|
82
|
+
suggest,
|
|
83
|
+
evaluation,
|
|
84
|
+
forcedAt
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Re-export of `tierToCode` for callers that want a stable mapping
|
|
89
|
+
* without depending on the threshold module directly.
|
|
90
|
+
*/
|
|
91
|
+
export { tierToCode };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Build the canonical artifact path for a given session/rid/role/idx/ext. */
|
|
2
|
+
export declare function artifactPath(projectRoot: string, sid: string, rid: string, role: string, idx: number, ext?: string): string;
|
|
3
|
+
/** Build the canonical shared channel path. */
|
|
4
|
+
export declare function sharedChannelPath(projectRoot: string, sid: string, rid: string, batchId: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Assert that `artifactPath` lives under
|
|
7
|
+
* `projectRoot/.peaks/_sub_agents/<sid>/artifacts/`. Rejects
|
|
8
|
+
* symlink/junction escapes and `..` segments.
|
|
9
|
+
*
|
|
10
|
+
* Throws an Error with `.code = 'INVALID_ARTIFACT_PATH'` on rejection.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertSafeArtifactPath(artifactPathInput: string, projectRoot: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Assert that `channelPath` lives under
|
|
15
|
+
* `projectRoot/.peaks/_sub_agents/<sid>/shared/`. Same R-2 logic as
|
|
16
|
+
* `assertSafeArtifactPath` but with a different canonical subdir.
|
|
17
|
+
*
|
|
18
|
+
* Throws an Error with `.code = 'INVALID_SHARED_CHANNEL_PATH'` on rejection.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertSafeSharedChannelPath(channelPathInput: string, projectRoot: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Soft-warn check on the artifact file name pattern. Returns null if
|
|
23
|
+
* the name matches `<rid>-<role>-<idx>.<ext>`, otherwise returns a
|
|
24
|
+
* warning string. Does NOT reject (the path is still in the canonical
|
|
25
|
+
* dir; the warning is for human/audit readability per G7.4.c).
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkArtifactNameConvention(artifactPathInput: string): string | null;
|