peaks-loop 4.0.28 → 4.0.30
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 +30 -0
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/dispatch-commands.d.ts +5 -41
- package/dist/cli/commands/dispatch-commands.js +59 -185
- package/dist/cli/commands/evidence-commands.d.ts +12 -0
- package/dist/cli/commands/evidence-commands.js +43 -0
- package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
- package/dist/cli/commands/fresh-context-commands.js +24 -0
- package/dist/cli/commands/job-commands.js +14 -1
- package/dist/cli/commands/request-commands.js +15 -97
- package/dist/cli/commands/request-format-helpers.d.ts +19 -0
- package/dist/cli/commands/request-format-helpers.js +102 -0
- package/dist/cli/commands/worktree-auth-commands.js +2 -472
- package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
- package/dist/cli/commands/worktree-lease-commands.js +500 -0
- package/dist/services/artifacts/request-artifact-service.js +13 -3
- package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
- package/dist/services/code/auto-compact-lifecycle.js +235 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +1 -222
- package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
- package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
- package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
- package/dist/services/context/build-dispatch-system-prompt.js +70 -3
- package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
- package/dist/services/dispatch/dispatch-record-types.js +15 -0
- package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
- package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
- package/dist/services/dispatch/dispatch-record-writer.js +3 -245
- package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
- package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
- package/dist/services/dispatch/isolation-lease.d.ts +43 -0
- package/dist/services/dispatch/isolation-lease.js +129 -0
- package/dist/services/evidence/evidence-generator.d.ts +26 -0
- package/dist/services/evidence/evidence-generator.js +349 -0
- package/dist/services/fresh-context/config.d.ts +1 -0
- package/dist/services/fresh-context/config.js +13 -0
- package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
- package/dist/services/fresh-context/fresh-context-block.js +40 -0
- package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
- package/dist/services/fresh-context/trigger-scan.js +29 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
- package/dist/services/skills/hooks-settings-service.d.ts +2 -64
- package/dist/services/skills/hooks-settings-service.js +2 -215
- package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
- package/dist/services/skills/skill-statusline-renderer.js +1 -184
- package/dist/services/skills/statusline-palette.d.ts +62 -0
- package/dist/services/skills/statusline-palette.js +190 -0
- package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
- package/dist/services/slice/slice-decompose-import-edges.js +102 -0
- package/dist/services/slice/slice-decompose-service.js +3 -198
- package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
- package/dist/services/slice/slice-decompose-tarjan.js +108 -0
- package/dist/services/standards/project-standards-service.d.ts +1 -9
- package/dist/services/standards/project-standards-service.js +3 -232
- package/dist/services/standards/standards-render.d.ts +23 -0
- package/dist/services/standards/standards-render.js +238 -0
- package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
- package/dist/services/standards/ui-library-dispatch-block.js +48 -0
- package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
- package/dist/services/workspace/reconcile-migrate.js +230 -0
- package/dist/services/workspace/reconcile-service.d.ts +0 -72
- package/dist/services/workspace/reconcile-service.js +3 -220
- package/dist/services/workspace/workspace-service.js +13 -2
- package/dist/shared/incrementing-number.d.ts +11 -0
- package/dist/shared/incrementing-number.js +16 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +6 -2
- package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
|
@@ -108,78 +108,6 @@ export declare function syncChangeMarker(projectRoot: string, canonicalSessionId
|
|
|
108
108
|
created: string | null;
|
|
109
109
|
error: string | null;
|
|
110
110
|
};
|
|
111
|
-
/**
|
|
112
|
-
* One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
|
|
113
|
-
*
|
|
114
|
-
* Move the legacy runtime files at:
|
|
115
|
-
* - `.peaks/.session.json`
|
|
116
|
-
* - `.peaks/.active-skill.json`
|
|
117
|
-
* - `.peaks/sop-state/`
|
|
118
|
-
* into their new canonical home at:
|
|
119
|
-
* - `.peaks/_runtime/session.json`
|
|
120
|
-
* - `.peaks/_runtime/active-skill.json`
|
|
121
|
-
* - `.peaks/_runtime/sop-state/`
|
|
122
|
-
*
|
|
123
|
-
* Behavior:
|
|
124
|
-
* - Idempotent: re-running on a tree that is already on the new
|
|
125
|
-
* layout produces `migratedFiles: []`.
|
|
126
|
-
* - Best-effort: uses `fs.renameSync` (atomic on POSIX, best-effort
|
|
127
|
-
* on Windows) and falls back to `copyFileSync` + `unlinkSync` if
|
|
128
|
-
* rename throws (e.g. cross-device move on Windows). Errors are
|
|
129
|
-
* collected per file and returned in the `errors` array so the
|
|
130
|
-
* reconcile envelope can surface them without blocking the rest of
|
|
131
|
-
* the migration.
|
|
132
|
-
* - Creates `.peaks/_runtime/` on demand if any of the old paths
|
|
133
|
-
* are present.
|
|
134
|
-
*
|
|
135
|
-
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the
|
|
136
|
-
* *old* relative paths (e.g. `.peaks/.session.json`) that were
|
|
137
|
-
* successfully moved, in move order. `errors` lists per-file
|
|
138
|
-
* failures with the old path and a human-readable message.
|
|
139
|
-
*/
|
|
140
|
-
export declare function migrateOldRuntimeState(projectRoot: string): {
|
|
141
|
-
migratedFiles: string[];
|
|
142
|
-
errors: Array<{
|
|
143
|
-
path: string;
|
|
144
|
-
message: string;
|
|
145
|
-
}>;
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
149
|
-
*
|
|
150
|
-
* Move the legacy per-session sub-agent state files at:
|
|
151
|
-
* - `.peaks/_runtime/<sid>/system/subagent-progress.json`
|
|
152
|
-
* - `.peaks/_runtime/<sid>/system/progress-spawn.json`
|
|
153
|
-
* into the new canonical home at:
|
|
154
|
-
* - `.peaks/_sub_agents/<sid>/subagent-progress.json`
|
|
155
|
-
* - `.peaks/_sub_agents/<sid>/progress-spawn.json`
|
|
156
|
-
*
|
|
157
|
-
* Behavior:
|
|
158
|
-
* - Idempotent: re-running on a tree that is already on the new layout
|
|
159
|
-
* produces `migratedFiles: []`.
|
|
160
|
-
* - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
|
|
161
|
-
* unlinkSync` if rename throws (e.g. cross-device move on Windows).
|
|
162
|
-
* - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
|
|
163
|
-
* subdir is only removed when it has zero other files, so a tree where
|
|
164
|
-
* the user had unrelated content in `system/` is left untouched.
|
|
165
|
-
* - New-path-wins: when both old and new files exist, the old file is
|
|
166
|
-
* removed (the new path is authoritative).
|
|
167
|
-
*
|
|
168
|
-
* Walks every discovered session — not just the canonical one — so a user
|
|
169
|
-
* with 6 pre-migration sessions gets all of them migrated in one reconcile
|
|
170
|
-
* pass.
|
|
171
|
-
*
|
|
172
|
-
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
|
|
173
|
-
* relative paths (e.g. `.peaks/_runtime/<sid>/system/subagent-progress.json`) that
|
|
174
|
-
* were successfully moved. `errors` lists per-file failures.
|
|
175
|
-
*/
|
|
176
|
-
export declare function migrateSubAgentState(projectRoot: string): {
|
|
177
|
-
migratedFiles: string[];
|
|
178
|
-
errors: Array<{
|
|
179
|
-
path: string;
|
|
180
|
-
message: string;
|
|
181
|
-
}>;
|
|
182
|
-
};
|
|
183
111
|
/**
|
|
184
112
|
* Top-level orchestrator. Wires migration (added in slice
|
|
185
113
|
* 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
|
|
@@ -16,43 +16,12 @@
|
|
|
16
16
|
* Pure hand-rolled; uses only node:fs, node:path, and the existing
|
|
17
17
|
* session-manager helper for writing the binding. No new dependencies.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
21
|
import { getSessionIdCanonical, setCurrentSessionBinding } from '../session/session-manager.js';
|
|
22
|
+
import { migrateOldRuntimeState, migrateSubAgentState } from './reconcile-migrate.js';
|
|
22
23
|
const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
|
|
23
24
|
const META_FILE = 'session.json';
|
|
24
|
-
// Sub-agent state file basenames (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
25
|
-
// The legacy location was `.peaks/_runtime/<sid>/system/<filename>`; the canonical new
|
|
26
|
-
// location is `.peaks/_sub_agents/<sid>/<filename>`. `migrateSubAgentState`
|
|
27
|
-
// moves the two files between these homes on every `reconcileWorkspace` run.
|
|
28
|
-
const SUB_AGENT_MIGRATION_FILES = [
|
|
29
|
-
'subagent-progress.json',
|
|
30
|
-
'progress-spawn.json'
|
|
31
|
-
];
|
|
32
|
-
const SUB_AGENTS_DIR = '_sub_agents';
|
|
33
|
-
// As of slice 2026-06-05-peaks-runtime-layer these old paths are the
|
|
34
|
-
// back-compat read-only fallbacks; the canonical new home is
|
|
35
|
-
// `.peaks/_runtime/`. `migrateOldRuntimeState` moves them to the new
|
|
36
|
-
// location on disk. The leading dot is dropped when computing the
|
|
37
|
-
// new basename (e.g. `.session.json` → `session.json`), so the new
|
|
38
|
-
// layout is `.peaks/_runtime/{session.json,active-skill.json,sop-state/}`.
|
|
39
|
-
const RUNTIME_OLD_PATHS = [
|
|
40
|
-
'.session.json',
|
|
41
|
-
'.active-skill.json',
|
|
42
|
-
'sop-state'
|
|
43
|
-
];
|
|
44
|
-
const RUNTIME_DIR = join('.peaks', '_runtime');
|
|
45
|
-
/**
|
|
46
|
-
* Map a legacy path basename (e.g. `.session.json`) to its canonical
|
|
47
|
-
* new basename (e.g. `session.json`). The dot is dropped so the new
|
|
48
|
-
* layer reads naturally. Directories pass through unchanged.
|
|
49
|
-
*/
|
|
50
|
-
function runtimeNewBasename(oldBasename) {
|
|
51
|
-
if (oldBasename.startsWith('.') && oldBasename.length > 1) {
|
|
52
|
-
return oldBasename.slice(1);
|
|
53
|
-
}
|
|
54
|
-
return oldBasename;
|
|
55
|
-
}
|
|
56
25
|
/**
|
|
57
26
|
* Walk the project root's `.peaks/` directory and return an entry per
|
|
58
27
|
* session dir matching the standard naming pattern, sorted by name
|
|
@@ -394,192 +363,6 @@ export function syncChangeMarker(projectRoot, canonicalSessionId) {
|
|
|
394
363
|
}
|
|
395
364
|
return { removed, created, error };
|
|
396
365
|
}
|
|
397
|
-
/**
|
|
398
|
-
* One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
|
|
399
|
-
*
|
|
400
|
-
* Move the legacy runtime files at:
|
|
401
|
-
* - `.peaks/.session.json`
|
|
402
|
-
* - `.peaks/.active-skill.json`
|
|
403
|
-
* - `.peaks/sop-state/`
|
|
404
|
-
* into their new canonical home at:
|
|
405
|
-
* - `.peaks/_runtime/session.json`
|
|
406
|
-
* - `.peaks/_runtime/active-skill.json`
|
|
407
|
-
* - `.peaks/_runtime/sop-state/`
|
|
408
|
-
*
|
|
409
|
-
* Behavior:
|
|
410
|
-
* - Idempotent: re-running on a tree that is already on the new
|
|
411
|
-
* layout produces `migratedFiles: []`.
|
|
412
|
-
* - Best-effort: uses `fs.renameSync` (atomic on POSIX, best-effort
|
|
413
|
-
* on Windows) and falls back to `copyFileSync` + `unlinkSync` if
|
|
414
|
-
* rename throws (e.g. cross-device move on Windows). Errors are
|
|
415
|
-
* collected per file and returned in the `errors` array so the
|
|
416
|
-
* reconcile envelope can surface them without blocking the rest of
|
|
417
|
-
* the migration.
|
|
418
|
-
* - Creates `.peaks/_runtime/` on demand if any of the old paths
|
|
419
|
-
* are present.
|
|
420
|
-
*
|
|
421
|
-
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the
|
|
422
|
-
* *old* relative paths (e.g. `.peaks/.session.json`) that were
|
|
423
|
-
* successfully moved, in move order. `errors` lists per-file
|
|
424
|
-
* failures with the old path and a human-readable message.
|
|
425
|
-
*/
|
|
426
|
-
export function migrateOldRuntimeState(projectRoot) {
|
|
427
|
-
const root = resolve(projectRoot);
|
|
428
|
-
const peaksRoot = join(root, '.peaks');
|
|
429
|
-
const newDir = join(root, RUNTIME_DIR);
|
|
430
|
-
const migratedFiles = [];
|
|
431
|
-
const errors = [];
|
|
432
|
-
for (const rel of RUNTIME_OLD_PATHS) {
|
|
433
|
-
const oldPath = join(peaksRoot, rel);
|
|
434
|
-
if (!existsSync(oldPath))
|
|
435
|
-
continue;
|
|
436
|
-
// Skip if the corresponding new path already exists — we treat the
|
|
437
|
-
// new path as authoritative when both exist, so the old file would
|
|
438
|
-
// only be stale data.
|
|
439
|
-
const newPath = join(newDir, runtimeNewBasename(rel));
|
|
440
|
-
if (existsSync(newPath)) {
|
|
441
|
-
// Best-effort cleanup of the stale old file so a re-run stays
|
|
442
|
-
// idempotent and the tree converges on the new layout.
|
|
443
|
-
try {
|
|
444
|
-
rmSync(oldPath, { recursive: true, force: true });
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
errors.push({
|
|
448
|
-
path: rel,
|
|
449
|
-
message: `Could not remove stale legacy file after migration: ${error instanceof Error ? error.message : String(error)}`
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
try {
|
|
455
|
-
// Ensure the new parent dir exists. `mkdirSync(dirname(newPath), { recursive: true })`
|
|
456
|
-
// covers both the file case (`.peaks/_runtime`) and the
|
|
457
|
-
// directory case (`.peaks/_runtime/sop-state`).
|
|
458
|
-
mkdirSync(dirname(newPath), { recursive: true });
|
|
459
|
-
try {
|
|
460
|
-
renameSync(oldPath, newPath);
|
|
461
|
-
}
|
|
462
|
-
catch (renameError) {
|
|
463
|
-
// Cross-device or locked-file fallback: copy + unlink.
|
|
464
|
-
const stat = lstatSync(oldPath);
|
|
465
|
-
if (stat.isDirectory()) {
|
|
466
|
-
// Recursive copy for the sop-state dir.
|
|
467
|
-
copyDirRecursiveSync(oldPath, newPath);
|
|
468
|
-
rmSync(oldPath, { recursive: true, force: true });
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
copyFileSync(oldPath, newPath);
|
|
472
|
-
unlinkSync(oldPath);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
migratedFiles.push(join('.peaks', rel));
|
|
476
|
-
}
|
|
477
|
-
catch (error) {
|
|
478
|
-
errors.push({
|
|
479
|
-
path: rel,
|
|
480
|
-
message: error instanceof Error ? error.message : String(error)
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return { migratedFiles, errors };
|
|
485
|
-
}
|
|
486
|
-
function copyDirRecursiveSync(src, dest) {
|
|
487
|
-
mkdirSync(dest, { recursive: true });
|
|
488
|
-
for (const name of readdirSync(src)) {
|
|
489
|
-
const childSrc = join(src, name);
|
|
490
|
-
const childDest = join(dest, name);
|
|
491
|
-
const stat = lstatSync(childSrc);
|
|
492
|
-
if (stat.isDirectory()) {
|
|
493
|
-
copyDirRecursiveSync(childSrc, childDest);
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
copyFileSync(childSrc, childDest);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
502
|
-
*
|
|
503
|
-
* Move the legacy per-session sub-agent state files at:
|
|
504
|
-
* - `.peaks/_runtime/<sid>/system/subagent-progress.json`
|
|
505
|
-
* - `.peaks/_runtime/<sid>/system/progress-spawn.json`
|
|
506
|
-
* into the new canonical home at:
|
|
507
|
-
* - `.peaks/_sub_agents/<sid>/subagent-progress.json`
|
|
508
|
-
* - `.peaks/_sub_agents/<sid>/progress-spawn.json`
|
|
509
|
-
*
|
|
510
|
-
* Behavior:
|
|
511
|
-
* - Idempotent: re-running on a tree that is already on the new layout
|
|
512
|
-
* produces `migratedFiles: []`.
|
|
513
|
-
* - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
|
|
514
|
-
* unlinkSync` if rename throws (e.g. cross-device move on Windows).
|
|
515
|
-
* - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
|
|
516
|
-
* subdir is only removed when it has zero other files, so a tree where
|
|
517
|
-
* the user had unrelated content in `system/` is left untouched.
|
|
518
|
-
* - New-path-wins: when both old and new files exist, the old file is
|
|
519
|
-
* removed (the new path is authoritative).
|
|
520
|
-
*
|
|
521
|
-
* Walks every discovered session — not just the canonical one — so a user
|
|
522
|
-
* with 6 pre-migration sessions gets all of them migrated in one reconcile
|
|
523
|
-
* pass.
|
|
524
|
-
*
|
|
525
|
-
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
|
|
526
|
-
* relative paths (e.g. `.peaks/_runtime/<sid>/system/subagent-progress.json`) that
|
|
527
|
-
* were successfully moved. `errors` lists per-file failures.
|
|
528
|
-
*/
|
|
529
|
-
export function migrateSubAgentState(projectRoot) {
|
|
530
|
-
const root = resolve(projectRoot);
|
|
531
|
-
const newDir = join(root, '.peaks', SUB_AGENTS_DIR);
|
|
532
|
-
const migratedFiles = [];
|
|
533
|
-
const errors = [];
|
|
534
|
-
for (const session of discoverSessions(projectRoot)) {
|
|
535
|
-
const oldSystemDir = join(session.path, 'system');
|
|
536
|
-
if (!existsSync(oldSystemDir))
|
|
537
|
-
continue;
|
|
538
|
-
const newSessionDir = join(newDir, session.sessionId);
|
|
539
|
-
mkdirSync(newSessionDir, { recursive: true });
|
|
540
|
-
for (const fname of SUB_AGENT_MIGRATION_FILES) {
|
|
541
|
-
const oldPath = join(oldSystemDir, fname);
|
|
542
|
-
const newPath = join(newSessionDir, fname);
|
|
543
|
-
if (!existsSync(oldPath))
|
|
544
|
-
continue;
|
|
545
|
-
if (existsSync(newPath)) {
|
|
546
|
-
// New path is authoritative; remove stale old file.
|
|
547
|
-
try {
|
|
548
|
-
rmSync(oldPath, { force: true });
|
|
549
|
-
}
|
|
550
|
-
catch { /* best effort */ } // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
try {
|
|
554
|
-
try {
|
|
555
|
-
renameSync(oldPath, newPath);
|
|
556
|
-
}
|
|
557
|
-
catch (renameError) {
|
|
558
|
-
// Cross-device or locked-file fallback: copy + unlink.
|
|
559
|
-
copyFileSync(oldPath, newPath);
|
|
560
|
-
unlinkSync(oldPath);
|
|
561
|
-
}
|
|
562
|
-
migratedFiles.push(join('.peaks', session.sessionId, 'system', fname));
|
|
563
|
-
}
|
|
564
|
-
catch (error) {
|
|
565
|
-
errors.push({
|
|
566
|
-
path: oldPath,
|
|
567
|
-
message: error instanceof Error ? error.message : String(error)
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// R-2 guard: only remove the legacy system/ dir when it has zero
|
|
572
|
-
// remaining files (the user might have unrelated content there).
|
|
573
|
-
try {
|
|
574
|
-
const remaining = readdirSync(oldSystemDir);
|
|
575
|
-
if (remaining.length === 0) {
|
|
576
|
-
rmdirSync(oldSystemDir);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
catch { /* best effort */ } // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
580
|
-
}
|
|
581
|
-
return { migratedFiles, errors };
|
|
582
|
-
}
|
|
583
366
|
/**
|
|
584
367
|
* Top-level orchestrator. Wires migration (added in slice
|
|
585
368
|
* 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
|
|
@@ -2,7 +2,7 @@ import { mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isDirectory } from 'peaks-loop-shared/fs';
|
|
5
|
-
import {
|
|
5
|
+
import { getSessionIdCanonical, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
|
|
6
6
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
7
7
|
/**
|
|
8
8
|
* Slice 2026-06-29-change-id-root-removal: list the immediate children of
|
|
@@ -257,7 +257,18 @@ export async function initWorkspace(options) {
|
|
|
257
257
|
// a parallel session without closing the previous one. Refuse to bind —
|
|
258
258
|
// this is the "strict" mode the user picked. The user must finish or delete
|
|
259
259
|
// the existing session first.
|
|
260
|
-
|
|
260
|
+
// Sub-agent session-rebind guard: read the existing binding via the
|
|
261
|
+
// canonicalize-on-read variant. A sub-agent re-running `peaks workspace
|
|
262
|
+
// init` may pass a project root whose spelling differs from the stored
|
|
263
|
+
// form (relative `"."`, symlink, separator/case variance on Windows).
|
|
264
|
+
// The strict `getSessionId` returns null on those spellings, which sent
|
|
265
|
+
// init down the "no prior binding → adopt" path and silently clobbered
|
|
266
|
+
// `.peaks/_runtime/session.json` with a phantom session, orphaning the
|
|
267
|
+
// parent's binding. `getSessionIdCanonical` resolves the stored form
|
|
268
|
+
// against the caller so a live parent binding is detected and the
|
|
269
|
+
// differing-id branch below refuses (or requires --allow-session-rebind)
|
|
270
|
+
// instead of silently overwriting.
|
|
271
|
+
const existingSessionId = getSessionIdCanonical(options.projectRoot);
|
|
261
272
|
let previousSessionId = null;
|
|
262
273
|
let bound = false;
|
|
263
274
|
if (existingSessionId === null) {
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* @returns Next available number (1, 2, 3, ...)
|
|
12
12
|
*/
|
|
13
13
|
export declare function getNextNumber(dirPath: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Convert a human-readable description into the kebab-case slug used in
|
|
16
|
+
* numbered artifact filenames. Lowercases, collapses non-alphanumerics to
|
|
17
|
+
* `-`, trims leading/trailing dashes, and caps at `MAX_FILENAME_SLUG_LENGTH`.
|
|
18
|
+
*
|
|
19
|
+
* Exported separately so lookup paths (e.g. `request-artifact-service.ts`)
|
|
20
|
+
* can compute the SAME slug the writer produced and match on-disk filenames
|
|
21
|
+
* case-insensitively — the writer lowercases, so a mixed-case request id
|
|
22
|
+
* like `2026-09-06-split-batchA` lands as `...-split-batcha.md`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function slugifyDescription(description: string): string;
|
|
14
25
|
export declare function buildNumberedFilename(number: number, description: string): string;
|
|
15
26
|
/**
|
|
16
27
|
* Get the next numbered file path in a directory.
|
|
@@ -44,13 +44,26 @@ export function getNextNumber(dirPath) {
|
|
|
44
44
|
// the slug may be at most 248 chars (giving 4 + 248 + 3 = 255 total).
|
|
45
45
|
const MAX_FILENAME_LENGTH = 255;
|
|
46
46
|
const MAX_FILENAME_SLUG_LENGTH = MAX_FILENAME_LENGTH - 7;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Convert a human-readable description into the kebab-case slug used in
|
|
49
|
+
* numbered artifact filenames. Lowercases, collapses non-alphanumerics to
|
|
50
|
+
* `-`, trims leading/trailing dashes, and caps at `MAX_FILENAME_SLUG_LENGTH`.
|
|
51
|
+
*
|
|
52
|
+
* Exported separately so lookup paths (e.g. `request-artifact-service.ts`)
|
|
53
|
+
* can compute the SAME slug the writer produced and match on-disk filenames
|
|
54
|
+
* case-insensitively — the writer lowercases, so a mixed-case request id
|
|
55
|
+
* like `2026-09-06-split-batchA` lands as `...-split-batcha.md`.
|
|
56
|
+
*/
|
|
57
|
+
export function slugifyDescription(description) {
|
|
58
|
+
return description
|
|
50
59
|
.toLowerCase()
|
|
51
60
|
.replace(/[^a-z0-9]+/g, '-')
|
|
52
61
|
.replace(/^-|-$/g, '')
|
|
53
62
|
.slice(0, MAX_FILENAME_SLUG_LENGTH);
|
|
63
|
+
}
|
|
64
|
+
export function buildNumberedFilename(number, description) {
|
|
65
|
+
const padded = String(number).padStart(3, '0');
|
|
66
|
+
const slug = slugifyDescription(description);
|
|
54
67
|
return `${padded}-${slug}.md`;
|
|
55
68
|
}
|
|
56
69
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.30",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"fzf": "^0.5.2",
|
|
102
102
|
"yaml": "^2.9.0",
|
|
103
103
|
"zod": "^4.4.3",
|
|
104
|
-
"peaks-loop-internal-runtime": "0.0.
|
|
105
|
-
"peaks-loop-mut": "0.1.
|
|
106
|
-
"peaks-loop-shared": "0.0.
|
|
107
|
-
"peaks-loop-shared-channel": "0.0.
|
|
104
|
+
"peaks-loop-internal-runtime": "0.0.15",
|
|
105
|
+
"peaks-loop-mut": "0.1.28",
|
|
106
|
+
"peaks-loop-shared": "0.0.64",
|
|
107
|
+
"peaks-loop-shared-channel": "0.0.32"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@changesets/cli": "2.31.1",
|
|
@@ -86,10 +86,10 @@ Two-layer enforcement — do NOT try to bypass either:
|
|
|
86
86
|
STOP. Run:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
peaks sub-agent dispatch rd --prompt '<your task>' --request-id <rid> --project . --batch-id <uuid> --json
|
|
89
|
+
peaks sub-agent dispatch rd --prompt '<your task>' --graph-node <nid> --workflow-id <wid> --request-id <rid> --project . --batch-id <uuid> --json
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The RD sub-agent owns the actual `Edit`/`Write`/`MultiEdit` tool calls against source code. The orchestrator only emits the dispatch request.
|
|
92
|
+
`--graph-node <nid>` is REQUIRED (RD §4 D4c). Prepare the node first: `peaks workflow init --skill peaks-code`, then `peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch`. The RD sub-agent owns the actual `Edit`/`Write`/`MultiEdit` tool calls against source code. The orchestrator only emits the dispatch request.
|
|
93
93
|
|
|
94
94
|
**Anti-pattern:** directly calling `Edit`/`Write`/`MultiEdit` on `src/**` from the orchestrator session because "the change is tiny" / "it's just one line" / "the dispatch overhead is too high" / "the LLM feels confident". None of these override the Code-Gate; the probe + the hook both fail-closed.
|
|
95
95
|
|
|
@@ -139,6 +139,10 @@ Sub-agents MUST NOT use raw `git worktree add`. The only authorized path is `pea
|
|
|
139
139
|
|
|
140
140
|
Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.87 / 1 / 2 / 2.3 / 2.5 / N / N+1 / N+2 + sub-agent sharing + boundaries). Read that file in full at session start; the sequence is MANDATORY.
|
|
141
141
|
|
|
142
|
+
## Peaks-Loop Fresh-context preflight (search-first, MANDATORY at orchestration-start)
|
|
143
|
+
|
|
144
|
+
Before the first planning action, run `peaks fresh-context preflight --prompt "<user request>" --json`. If `data.triggered` is true, search Context7 (priority, ~30s timeout) then WebSearch (fallback), synthesize ≤5 binding directives (用 X / 别用 Y / 因为 Z), and write the `## Fresh context` block (with the 以下信息优先于训练知识,冲突时以此为准 framing line) to `.peaks/_runtime/<sessionId>/fresh-context.md` — the RD/PRD dispatch site injects it automatically. Fail-soft (search failure → no block, slice continues). Kill-switch: `peaks config set --key freshContext.enabled --value false`. → `references/fresh-context-preflight.md`.
|
|
145
|
+
|
|
142
146
|
## Peaks-Loop Step 0.8 — Job-shape detection (BLOCKING on LLM judgement)
|
|
143
147
|
|
|
144
148
|
> **BLOCKING on LLM judgement.** Before Step 1 mode selection, Code MUST surface a Job-shape decision via `peaks code detect-job` and persist it to `.peaks/_runtime/<sessionId>/job-shape.json`. The CLI is a recorder + gate — the LLM makes the judgement; downstream steps call `read-job-shape` and refuse if missing. See `references/step-0-8-gate.md` for the full contract. Skipping `peaks code detect-job` blocks the workflow at the next `read-job-shape` call (`JOB_SHAPE_NOT_DECIDED`).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Fresh-context preflight (search-first)
|
|
2
|
+
|
|
3
|
+
> **Slice 2026-09-07-search-first-preflight.** Signal-triggered preflight that
|
|
4
|
+
> runs at **orchestration-start, before the first planning action**, for every
|
|
5
|
+
> peaks-* orchestrator (peaks-code, peaks-prd, peaks-rd, peaks-qa, … — not
|
|
6
|
+
> peaks-code specific). It hedges against model training-data lag by searching
|
|
7
|
+
> for fresh context and injecting binding directives into the PRD/RD dispatch.
|
|
8
|
+
|
|
9
|
+
## When
|
|
10
|
+
|
|
11
|
+
Run the deterministic trigger scan as the very first step of a workflow, on the
|
|
12
|
+
user's raw request text:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
peaks fresh-context preflight --prompt "<user request>" --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Read the `--json` envelope's `data` object:
|
|
19
|
+
|
|
20
|
+
| field | meaning |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `data.triggered` | `true` → run the search + synthesis below |
|
|
23
|
+
| `data.forced` | `true` → the user explicitly asked for a live search (联网搜 / 查最新 / 搜一下) |
|
|
24
|
+
| `data.signals` | the signal keywords that matched (升级 / 迁移 / 最新 / latest / new / 版本 / 兼容 / breaking / upgrade / migrate / 新框架 / 推荐库 / 选型) |
|
|
25
|
+
| `data.enabled` | the `freshContext.enabled` kill-switch (false → no-op) |
|
|
26
|
+
|
|
27
|
+
If `data.triggered` is `false` (no signal, or kill-switch off), **do nothing** —
|
|
28
|
+
the workflow proceeds exactly as before (byte-identical dispatch prompts).
|
|
29
|
+
|
|
30
|
+
## Search (only when `triggered: true`)
|
|
31
|
+
|
|
32
|
+
1. **Context7** (priority, `@upstash/context7-mcp`, ~30s timeout).
|
|
33
|
+
2. **WebSearch** fallback when Context7 returns empty / errors / times out.
|
|
34
|
+
|
|
35
|
+
Both fail-soft: if Context7 times out AND WebSearch fails, the slice continues
|
|
36
|
+
with **no block and no hard error** (acceptance criterion #8).
|
|
37
|
+
|
|
38
|
+
## Synthesize (≤5 binding directives)
|
|
39
|
+
|
|
40
|
+
From the search results, synthesize **≤5 binding directives**, each in the
|
|
41
|
+
imperative form **"use X / do NOT use Y / because Z"** (用 X / 别用 Y / 因为 Z):
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
- 用 React 19 的 `use()`(因为 18 的并发渲染 API 已过时)
|
|
45
|
+
- 别用 `react-router-dom` v6 的 `Switch`(因为 v7 已移除,冲突时以此为准)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Keep it to **5 or fewer** lines. Filter SEO noise — only keep directives that
|
|
49
|
+
change a decision the model would otherwise get wrong from training data.
|
|
50
|
+
|
|
51
|
+
## Write the block
|
|
52
|
+
|
|
53
|
+
Wrap the directives in a `## Fresh context` block with the authoritative
|
|
54
|
+
framing line (M1), and write it to
|
|
55
|
+
`.peaks/_runtime/<sessionId>/fresh-context.md`:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
## Fresh context
|
|
59
|
+
|
|
60
|
+
以下信息优先于训练知识,冲突时以此为准。
|
|
61
|
+
|
|
62
|
+
1. 用 X(因为 Z)
|
|
63
|
+
2. 别用 Y(因为 Z)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The RD/PRD dispatch site auto-injects this block (after the project-stack
|
|
67
|
+
block, before the memory/task content). No manual step is needed beyond
|
|
68
|
+
writing the file.
|
|
69
|
+
|
|
70
|
+
## Kill-switch
|
|
71
|
+
|
|
72
|
+
`peaks config set --key freshContext.enabled --value false` disables the whole
|
|
73
|
+
preflight (no search, no injection). The default is enabled (absent key →
|
|
74
|
+
`true`).
|
|
75
|
+
|
|
76
|
+
## RD planning note
|
|
77
|
+
|
|
78
|
+
RD's planning artifact MUST record a "Fresh context 遵循" section listing which
|
|
79
|
+
of the ≤5 directives the implementation followed (acceptance criterion #6).
|
|
80
|
+
QA / code-review then verify the implemented version/API against the block
|
|
81
|
+
(acceptance criterion #7).
|
|
@@ -39,9 +39,23 @@ stays IDE-agnostic.
|
|
|
39
39
|
**Command**:
|
|
40
40
|
|
|
41
41
|
```
|
|
42
|
-
peaks sub-agent dispatch <role> --prompt <text> [--request-id <rid>] [--session-id <sid>] [--project <repo>] [--batch-id <uuid>] --json
|
|
42
|
+
peaks sub-agent dispatch <role> --prompt <text> --graph-node <nid> [--workflow-id <wid>] [--request-id <rid>] [--session-id <sid>] [--project <repo>] [--batch-id <uuid>] --json
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
> **`--graph-node` is REQUIRED (RD §4 D4c, effective 4.0.8).** Every
|
|
46
|
+
> single dispatch must bind to a prepared workflow graph node. Prepare
|
|
47
|
+
> one first:
|
|
48
|
+
>
|
|
49
|
+
> ```
|
|
50
|
+
> peaks workflow init --skill peaks-code # → returns <wid>
|
|
51
|
+
> peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch
|
|
52
|
+
> ```
|
|
53
|
+
>
|
|
54
|
+
> then dispatch with `--graph-node <nid> --workflow-id <wid>`. Omitting
|
|
55
|
+
> `--graph-node` rejects with `PEAKS_GRAPH_NODE_REQUIRED`; a node that is
|
|
56
|
+
> not prepared (or has the wrong kind) rejects with
|
|
57
|
+
> `PEAKS_GRAPH_NODE_NOT_PREPARED` / `PEAKS_GRAPH_NODE_KIND_INVALID`.
|
|
58
|
+
|
|
45
59
|
**Envelope** (AC-8) — **2.1.0** (slice 2026-06-23-audit-4th #E1):
|
|
46
60
|
|
|
47
61
|
> **Audit-3rd #4 + audit-4th #E1**: the `data.prompt` field was
|
|
@@ -342,16 +356,20 @@ grown unboundedly without this fix).
|
|
|
342
356
|
When writing a SKILL.md that fans out sub-agents:
|
|
343
357
|
|
|
344
358
|
1. Use `peaks sub-agent dispatch <role>` (never `Task(...)`).
|
|
345
|
-
2.
|
|
359
|
+
2. Prepare the graph node first (RD §4 D4c — `--graph-node` is required):
|
|
360
|
+
`peaks workflow init --skill peaks-code`, then
|
|
361
|
+
`peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch`.
|
|
362
|
+
3. Issue all dispatches in a single message; the LLM will fire all
|
|
346
363
|
returned toolCalls in parallel.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
364
|
+
4. Pass `--graph-node <nid> --workflow-id <wid>` on every dispatch, plus
|
|
365
|
+
`--request-id` and `--session-id` (or omit and let the CLI resolve the
|
|
366
|
+
active session).
|
|
367
|
+
5. The sub-agent prompt **must** include the heartbeat instruction
|
|
350
368
|
(30 s cadence; override via `heartbeatIntervalSec` if needed).
|
|
351
|
-
|
|
369
|
+
6. After the fan-out returns, the Dispatcher reducer reads the
|
|
352
370
|
dispatch record + the artifacts the sub-agents wrote, marks
|
|
353
371
|
`disposed: true` on each record, and advances the state machine.
|
|
354
|
-
|
|
372
|
+
7. The poller handles the 5-min stale case as a warning, never as a
|
|
355
373
|
failure. The user is the one who decides to cancel.
|
|
356
374
|
|
|
357
375
|
## Cross-reference
|
|
@@ -380,6 +398,7 @@ peaks sub-agent dispatch <role> \
|
|
|
380
398
|
hand back prose.', plus the heartbeat instruction: 'While running, call
|
|
381
399
|
peaks sub-agent heartbeat --record <dispatchRecordPath> --status <state> --progress <pct> --note \"<text>\"
|
|
382
400
|
at least every 30 seconds.'>" \
|
|
401
|
+
--graph-node <nid> --workflow-id <wid> \
|
|
383
402
|
--request-id <rid> --session-id <session-id> --project <repo> --json
|
|
384
403
|
```
|
|
385
404
|
|
|
@@ -426,7 +445,7 @@ peaks skill presence:set peaks-code --project <repo> --mode <mode> --gate swarm-
|
|
|
426
445
|
|
|
427
446
|
## Detached Mode (Phase A, slice 2026-08-10)
|
|
428
447
|
|
|
429
|
-
`peaks sub-agent dispatch <role> --prompt <text> --request-id <rid> --mode detached --vendor claude|codex|copilot [--no-throttle --max-concurrent <N>] --json` spawns a real OS process independent of the orchestrator IDE session.
|
|
448
|
+
`peaks sub-agent dispatch <role> --prompt <text> --graph-node <nid> --workflow-id <wid> --request-id <rid> --mode detached --vendor claude|codex|copilot [--no-throttle --max-concurrent <N>] --json` spawns a real OS process independent of the orchestrator IDE session. `--graph-node` remains required here (RD §4 D4c).
|
|
430
449
|
|
|
431
450
|
- **Cross-platform spawn**: Windows uses `DETACHED_PROCESS` + `CREATE_NEW_PROCESS_GROUP`; POSIX uses `setsid` + `nohup`. Implementation: `packages/peaks-loop-internal-runtime/src/process-supervisor.ts`.
|
|
432
451
|
- **Minimum-context prompt**: `PromptBuilder` emits a 5–8KB slice `{rid, role, vendor, files, refs}` plus the verbatim `<peaks-auto-compact>` marker. The forbidden marker `@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@` MUST NOT appear in any prompt (unit-tested; regression fails vitest).
|