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.
Files changed (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli/commands/_register.js +4 -0
  3. package/dist/cli/commands/dispatch-commands.d.ts +5 -41
  4. package/dist/cli/commands/dispatch-commands.js +59 -185
  5. package/dist/cli/commands/evidence-commands.d.ts +12 -0
  6. package/dist/cli/commands/evidence-commands.js +43 -0
  7. package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
  8. package/dist/cli/commands/fresh-context-commands.js +24 -0
  9. package/dist/cli/commands/job-commands.js +14 -1
  10. package/dist/cli/commands/request-commands.js +15 -97
  11. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  12. package/dist/cli/commands/request-format-helpers.js +102 -0
  13. package/dist/cli/commands/worktree-auth-commands.js +2 -472
  14. package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
  15. package/dist/cli/commands/worktree-lease-commands.js +500 -0
  16. package/dist/services/artifacts/request-artifact-service.js +13 -3
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
  18. package/dist/services/code/auto-compact-lifecycle.js +235 -0
  19. package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
  20. package/dist/services/code/auto-compact-orchestrator.js +1 -222
  21. package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
  22. package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
  23. package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
  24. package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
  25. package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
  26. package/dist/services/context/build-dispatch-system-prompt.js +70 -3
  27. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  28. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  29. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  31. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  32. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  33. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  34. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  35. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  36. package/dist/services/dispatch/isolation-lease.js +129 -0
  37. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  38. package/dist/services/evidence/evidence-generator.js +349 -0
  39. package/dist/services/fresh-context/config.d.ts +1 -0
  40. package/dist/services/fresh-context/config.js +13 -0
  41. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  42. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  43. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  44. package/dist/services/fresh-context/trigger-scan.js +29 -0
  45. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  47. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  48. package/dist/services/skills/hooks-settings-service.js +2 -215
  49. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  50. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  51. package/dist/services/skills/statusline-palette.d.ts +62 -0
  52. package/dist/services/skills/statusline-palette.js +190 -0
  53. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  54. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  55. package/dist/services/slice/slice-decompose-service.js +3 -198
  56. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  57. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  58. package/dist/services/standards/project-standards-service.d.ts +1 -9
  59. package/dist/services/standards/project-standards-service.js +3 -232
  60. package/dist/services/standards/standards-render.d.ts +23 -0
  61. package/dist/services/standards/standards-render.js +238 -0
  62. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  63. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  64. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  65. package/dist/services/workspace/reconcile-migrate.js +230 -0
  66. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  67. package/dist/services/workspace/reconcile-service.js +3 -220
  68. package/dist/services/workspace/workspace-service.js +13 -2
  69. package/dist/shared/incrementing-number.d.ts +11 -0
  70. package/dist/shared/incrementing-number.js +16 -3
  71. package/package.json +5 -5
  72. package/skills/peaks-code/SKILL.md +6 -2
  73. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  74. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
@@ -30,21 +30,9 @@ import { assertSafeDispatchRecordPath, dispatchRecordPath } from '../security/sa
30
30
  import { withFileLockSync } from 'peaks-loop-shared-channel';
31
31
  import { isStageLabel } from './stage-enum.js';
32
32
  import { emitLeaseEvent } from '../observability/observability-service.js';
33
- /**
34
- * PRD-002b slice 2 — extract dispatch-record size budgets (max-prompt
35
- * bytes, note truncation cap) + time-math primitives so the
36
- * no-magic-numbers rule stops flagging the writer pipeline.
37
- */
38
- const BYTES_PER_KB = 1024;
39
- const MAX_PROMPT_KB = 256;
40
- const MAX_PROMPT_BYTES = MAX_PROMPT_KB * BYTES_PER_KB;
41
- const NOTE_MAX_CHARS = 200;
42
- const MS_PER_SECOND = 1_000;
43
- const SECONDS_PER_MINUTE = 60;
44
- const MINUTES_PER_HOUR = 60;
45
- const HOURS_PER_DAY = 24;
46
- const MS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
47
- const REDACTION_MAX_SCAN_DEPTH = 20;
33
+ import { upgradeRecord } from './dispatch-record-upgrade.js';
34
+ import { MAX_PROMPT_BYTES, MS_PER_DAY, NOTE_MAX_CHARS } from './dispatch-record-types.js';
35
+ export { isDispatchStatus, isOutcome } from './dispatch-record-upgrade.js';
48
36
  /** Write a new dispatch record (G2 + G5 + G6). Returns the absolute path. */
49
37
  export function writeInitialDispatchRecord(input) {
50
38
  const { projectRoot, sessionId, requestId, role, prompt, toolCall, batchId } = input;
@@ -674,236 +662,6 @@ export function readRecords(paths) {
674
662
  }
675
663
  return out;
676
664
  }
677
- function upgradeRecord(parsed) {
678
- if (!isObject(parsed)) {
679
- throw new Error('Dispatch record root must be an object');
680
- }
681
- const obj = parsed;
682
- // Slice 4.0.8: 3.2 → 4.0.0 schema bump. Phase A Task 8: 4.0.0 → 4.1.0
683
- // (additive). The literal type narrows to '4.1.0' but legacy v4.0.0 /
684
- // v3.2 / v3.1 / 3 / 2 / 1 records are accepted transparently and
685
- // upgraded on read.
686
- const rawVersion = obj.version;
687
- if (rawVersion !== '4.1.0' && rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
688
- throw new Error(`Dispatch record version mismatch: expected '4.1.0', '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
689
- 'The v1 → v4.1.0 migration is in-file; records from much older or newer builds must be regenerated.');
690
- }
691
- const legacy = parseUpgradeRecordLegacyFields(obj);
692
- const migration = parseUpgradeRecordMigrationFields(obj);
693
- return {
694
- version: '4.1.0',
695
- createdAt: legacy.createdAt,
696
- completedAt: legacy.completedAt,
697
- outcome: legacy.outcome,
698
- artifactPaths: legacy.artifactPaths,
699
- disposed: legacy.disposed,
700
- disposedAt: legacy.disposedAt,
701
- role: legacy.role,
702
- requestId: legacy.requestId,
703
- sessionId: legacy.sessionId,
704
- prompt: legacy.prompt,
705
- toolCall: legacy.toolCall,
706
- batchId: legacy.batchId,
707
- heartbeats: legacy.heartbeats,
708
- lastBeatAt: legacy.lastBeatAt,
709
- status: legacy.status,
710
- stage: migration.stage,
711
- leaseId: migration.leaseId,
712
- isolationStartedAt: migration.isolationStartedAt,
713
- serviceKill: migration.serviceKill,
714
- mergeBackAttempts: migration.mergeBackAttempts,
715
- workflowId: migration.workflowId,
716
- graphNodeId: migration.graphNodeId,
717
- graphRef: migration.graphRef,
718
- // Phase A Task 8: detached sub-agent fields. Legacy records
719
- // (pre-4.1.0) default mode='in-process', vendor=null,
720
- // autoCompactEvents=[], tokenUsage=null. See
721
- // parseUpgradeRecordMigrationFields for the per-field
722
- // validation rules.
723
- mode: migration.mode,
724
- vendor: migration.vendor,
725
- autoCompactEvents: migration.autoCompactEvents,
726
- tokenUsage: migration.tokenUsage
727
- };
728
- }
729
- /**
730
- * Parse the v1..v3.2 core fields of a legacy dispatch record.
731
- * PRD-002b slice 6: extracted from `upgradeRecord` so the reader
732
- * stays under the `max-lines-per-function: 50` ESLint ceiling.
733
- * Behavior is byte-identical to the previous inline block.
734
- */
735
- function parseUpgradeRecordLegacyFields(obj) {
736
- const role = stringField(obj, 'role');
737
- const requestId = stringField(obj, 'requestId');
738
- const sessionId = stringField(obj, 'sessionId');
739
- const prompt = stringField(obj, 'prompt');
740
- // Slice 2026-06-23-audit-4th #C2: preserve toolCallVersion on read.
741
- // Pre-versioning records default to '2.0.0' (the pre-#C2 implicit
742
- // shape; matches the version stamped by every current dispatcher).
743
- const rawToolCall = obj.toolCall;
744
- if (!isObject(rawToolCall) || typeof rawToolCall.name !== 'string') {
745
- throw new Error('Dispatch record toolCall must be { name, args }');
746
- }
747
- const toolCall = {
748
- name: rawToolCall.name,
749
- args: (isObject(rawToolCall.args) ? rawToolCall.args : {}),
750
- ...(typeof rawToolCall.toolCallVersion === 'string' ? { toolCallVersion: rawToolCall.toolCallVersion } : { toolCallVersion: '2.0.0' })
751
- };
752
- const createdAt = stringField(obj, 'createdAt');
753
- const heartbeats = Array.isArray(obj.heartbeats)
754
- ? obj.heartbeats.filter(isValidHeartbeat)
755
- : [];
756
- const lastBeatAt = typeof obj.lastBeatAt === 'string' ? obj.lastBeatAt : null;
757
- // Slice 2026-07-29-dispatch-stall-governance / S1 (UQ-1) — `no-execution`
758
- // keeps its natural "dispatched, never executed" reading; an unparseable
759
- // status field now resolves to a *distinct* `unreadable` label so the
760
- // caller can tell "corrupt record" apart from "record written, no first
761
- // heartbeat" (which is the new `never-started` state).
762
- const status = isDispatchStatus(obj.status)
763
- ? obj.status
764
- : 'unreadable';
765
- const completedAt = typeof obj.completedAt === 'string' ? obj.completedAt : null;
766
- const outcome = isOutcome(obj.outcome) ? obj.outcome : 'no-execution';
767
- const artifactPaths = Array.isArray(obj.artifactPaths)
768
- ? obj.artifactPaths.filter((p) => typeof p === 'string')
769
- : [];
770
- const disposed = obj.disposed === true;
771
- const disposedAt = typeof obj.disposedAt === 'string' ? obj.disposedAt : null;
772
- const batchId = typeof obj.batchId === 'string' && obj.batchId.length > 0
773
- ? obj.batchId
774
- : 'legacy-batch';
775
- return {
776
- role,
777
- requestId,
778
- sessionId,
779
- prompt,
780
- toolCall,
781
- createdAt,
782
- heartbeats,
783
- lastBeatAt,
784
- status,
785
- completedAt,
786
- outcome,
787
- artifactPaths,
788
- disposed,
789
- disposedAt,
790
- batchId
791
- };
792
- }
793
- /**
794
- * Parse the post-v3 migration fields of a legacy dispatch record.
795
- * PRD-002b slice 6: extracted from `upgradeRecord` so the reader
796
- * stays under the `max-lines-per-function: 50` ESLint ceiling.
797
- * Behavior is byte-identical to the previous inline block.
798
- */
799
- function parseUpgradeRecordMigrationFields(obj) {
800
- return {
801
- // Slice 2026-07-29-dispatch-stall-governance / S5 (AC-5.1 / PB-2)
802
- // — legacy records (pre-slice) had no `stage` field. The reader
803
- // defaults to `null` so the watch surface can tell "no stage ever
804
- // emitted" apart from "stage: ''" (which is itself a *valid*
805
- // round-trip through the writer — an empty stage is rejected by
806
- // `setStage`, but a record that round-tripped through a non-strict
807
- // tool would land here).
808
- stage: typeof obj.stage === 'string' && obj.stage.length > 0 ? obj.stage : null,
809
- // Slice 2026-07-29-worktree-l2-extended Part 3.A: legacy records
810
- // have no `leaseId`; default to `null` so the auto-release hook
811
- // in `markCompleted` is a clean no-op for them.
812
- leaseId: typeof obj.leaseId === 'string' && /^[a-f0-9]{16}$/.test(obj.leaseId)
813
- ? obj.leaseId
814
- : null,
815
- // Slice 2026-07-29-worktree-l2-extended Part 7: v3 → v3.1
816
- // migration. Legacy records have no `isolationStartedAt`; default
817
- // to `null`. v3.1 readers can treat the field as opt-in.
818
- isolationStartedAt: typeof obj.isolationStartedAt === 'string' && obj.isolationStartedAt.length > 0
819
- ? obj.isolationStartedAt
820
- : null,
821
- // Slice 2026-08-01-subagent-merge-and-e2e (Task 7): v3.1 → v3.2
822
- // migration. Legacy v3.1 records have no `serviceKill` or
823
- // `mergeBackAttempts` fields. Default to [] and 0 so the
824
- // merge-back-runner (Task 9) can read either schema on disk.
825
- serviceKill: Array.isArray(obj.serviceKill)
826
- ? (obj.serviceKill.filter((e) => {
827
- if (typeof e !== 'object' || e === null)
828
- return false;
829
- const o = e;
830
- return typeof o.pid === 'number' && typeof o.name === 'string' && typeof o.signal === 'string' && (o.exitCode === null || typeof o.exitCode === 'number');
831
- }))
832
- : [],
833
- mergeBackAttempts: typeof obj.mergeBackAttempts === 'number' && Number.isFinite(obj.mergeBackAttempts) && obj.mergeBackAttempts >= 0
834
- ? Math.floor(obj.mergeBackAttempts)
835
- : 0,
836
- // Slice 4.0.8: 3.2 → 4.0.0 migration. v3.2 records on disk
837
- // pre-date the workflow-graph binding; default all three
838
- // fields to `null` so a legacy record upgrades transparently.
839
- workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
840
- graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
841
- graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null,
842
- // Phase A Task 8: 4.0.0 → 4.1.0 migration. Pre-4.1.0 records
843
- // have no mode / vendor / autoCompactEvents / tokenUsage
844
- // fields. Default to safe in-process / null / [] / null so
845
- // legacy records upgrade transparently without breaking
846
- // consumers (e.g. the dashboard, the merge-back-runner).
847
- mode: obj.mode === 'detached' ? 'detached' : 'in-process',
848
- vendor: obj.vendor === 'claude' || obj.vendor === 'codex' || obj.vendor === 'copilot' ? obj.vendor : null,
849
- autoCompactEvents: Array.isArray(obj.autoCompactEvents)
850
- ? obj.autoCompactEvents.filter((e) => typeof e?.at === 'number' &&
851
- (e?.threshold === '0.85' || e?.threshold === '0.95') &&
852
- typeof e?.tokensBefore === 'number' &&
853
- typeof e?.tokensAfter === 'number')
854
- : [],
855
- tokenUsage: typeof obj.tokenUsage === 'object' && obj.tokenUsage !== null && typeof obj.tokenUsage.promptTokens === 'number' && typeof obj.tokenUsage.completionTokens === 'number'
856
- ? {
857
- promptTokens: obj.tokenUsage.promptTokens,
858
- completionTokens: obj.tokenUsage.completionTokens,
859
- ...(typeof obj.tokenUsage.totalCostUsd === 'number'
860
- ? { totalCostUsd: obj.tokenUsage.totalCostUsd }
861
- : {}),
862
- }
863
- : null
864
- };
865
- }
866
- function isObject(v) {
867
- return typeof v === 'object' && v !== null && !Array.isArray(v);
868
- }
869
- function stringField(obj, key) {
870
- const v = obj[key];
871
- if (typeof v !== 'string') {
872
- throw new Error(`Dispatch record field '${key}' must be a string (got ${typeof v})`);
873
- }
874
- return v;
875
- }
876
- function isValidHeartbeat(v) {
877
- if (!isObject(v))
878
- return false;
879
- return (typeof v.at === 'string' &&
880
- isHeartbeatStatus(v.status) &&
881
- typeof v.progress === 'number' &&
882
- (v.note === null || typeof v.note === 'string'));
883
- }
884
- function isHeartbeatStatus(v) {
885
- return (v === 'queued' || v === 'running' || v === 'finalizing' ||
886
- v === 'done' || v === 'failed' || v === 'stale' ||
887
- // Slice 2026-07-29-dispatch-stall-governance / S2 — accept the
888
- // S1 terminal members so a sub-agent can report `cancelled`,
889
- // `no-execution`, `never-started`, or `unreadable` through the
890
- // heartbeat CLI.
891
- v === 'cancelled' || v === 'no-execution' ||
892
- v === 'never-started' || v === 'unreadable');
893
- }
894
- function isDispatchStatus(v) {
895
- return (v === 'queued' || v === 'running' || v === 'finalizing' ||
896
- v === 'done' || v === 'failed' || v === 'cancelled' ||
897
- v === 'no-execution' || v === 'stale' ||
898
- // Slice 2026-07-29-dispatch-stall-governance / S1 — accept the two
899
- // new terminal members from the startup-timeout service.
900
- v === 'never-started' || v === 'unreadable');
901
- }
902
- function isOutcome(v) {
903
- return (v === 'success' || v === 'failed' || v === 'timeout' ||
904
- v === 'cancelled' || v === 'no-execution');
905
- }
906
- export { isDispatchStatus, isOutcome };
907
665
  function writeAtomic(path, record) {
908
666
  const dir = dirname(path);
909
667
  // Slice 2026-06-23-audit-3rd #11: skip mkdirSync when the dir already
@@ -0,0 +1,43 @@
1
+ /**
2
+ * F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
3
+ * §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
4
+ * `git ls-files <glob>` against `projectRoot` and returns the matching
5
+ * tracked file paths (relative to projectRoot). Empty array when no
6
+ * files match (e.g. untracked new file, wrong glob, not a git repo).
7
+ *
8
+ * Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
9
+ * is "the file the sub-agent claims to have written must ACTUALLY be
10
+ * tracked by git" — `git ls-files` enforces that contract; `fs.glob`
11
+ * would happily return untracked-but-on-disk files (false-positive
12
+ * for the fake-green gate).
13
+ *
14
+ * Failure modes (best-effort, never throws):
15
+ * - git not on PATH → empty array (`ENOENT` swallowed)
16
+ * - not a git repo → empty array (git exits non-zero)
17
+ * - glob matches zero tracked files → empty array
18
+ *
19
+ * Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
20
+ * The export is intentional — the helper has zero side effects and
21
+ * keeps the dispatch action handler small.
22
+ */
23
+ export declare function runGitLsFiles(projectRoot: string, glob: string): readonly string[];
24
+ /**
25
+ * `dispatchSubAgent` is the thin programmatic wrapper the integration
26
+ * test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
27
+ * It enforces --graph-node required BEFORE any record write so a
28
+ * caller can't bypass the CLI's requiredOption guard. Throws a typed
29
+ * error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
30
+ */
31
+ export declare function dispatchSubAgent(input: {
32
+ projectRoot: string;
33
+ role: string;
34
+ prompt: string;
35
+ sessionId?: string;
36
+ graphNode?: string;
37
+ workflowId?: string;
38
+ graphRef?: string;
39
+ }): Promise<{
40
+ role: string;
41
+ toolCall: unknown;
42
+ dispatchRecordPath: string | null;
43
+ }>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
3
+ * §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
4
+ * `git ls-files <glob>` against `projectRoot` and returns the matching
5
+ * tracked file paths (relative to projectRoot). Empty array when no
6
+ * files match (e.g. untracked new file, wrong glob, not a git repo).
7
+ *
8
+ * Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
9
+ * is "the file the sub-agent claims to have written must ACTUALLY be
10
+ * tracked by git" — `git ls-files` enforces that contract; `fs.glob`
11
+ * would happily return untracked-but-on-disk files (false-positive
12
+ * for the fake-green gate).
13
+ *
14
+ * Failure modes (best-effort, never throws):
15
+ * - git not on PATH → empty array (`ENOENT` swallowed)
16
+ * - not a git repo → empty array (git exits non-zero)
17
+ * - glob matches zero tracked files → empty array
18
+ *
19
+ * Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
20
+ * The export is intentional — the helper has zero side effects and
21
+ * keeps the dispatch action handler small.
22
+ */
23
+ export function runGitLsFiles(projectRoot, glob) {
24
+ try {
25
+ const { execFileSync } = require('node:child_process');
26
+ const stdout = execFileSync('git', ['ls-files', '--', glob], { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
27
+ return stdout.split('\n').filter((line) => line.length > 0);
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ }
33
+ /* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
34
+ /**
35
+ * `dispatchSubAgent` is the thin programmatic wrapper the integration
36
+ * test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
37
+ * It enforces --graph-node required BEFORE any record write so a
38
+ * caller can't bypass the CLI's requiredOption guard. Throws a typed
39
+ * error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
40
+ */
41
+ export async function dispatchSubAgent(input) {
42
+ if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
43
+ const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
44
+ err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
45
+ throw err;
46
+ }
47
+ // The integration test only checks the rejection path; the success
48
+ // path is exercised by the existing CLI command. Return a minimal
49
+ // stub so any future programmatic caller has a stable surface.
50
+ return {
51
+ role: input.role,
52
+ toolCall: null,
53
+ dispatchRecordPath: null,
54
+ };
55
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
3
+ * lease by shelling out to `peaks worktree spawn` (avoid re-implementing
4
+ * the lease-write + git-worktree-add sequence in this file). The CLI
5
+ * does the lease write, the git worktree add, AND the error handling;
6
+ * we just parse the JSON envelope and surface the leaseId + path.
7
+ *
8
+ * Throws on spawn failure; the caller converts the error to a
9
+ * ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
10
+ * dispatch is already async, the lease is a few hundred ms of FS work,
11
+ * and we need the leaseId before we build the dispatch record.
12
+ */
13
+ export declare function spawnWorktreeLease(args: {
14
+ projectRoot: string;
15
+ sessionId: string;
16
+ rid: string;
17
+ role: string;
18
+ purpose: string;
19
+ }): Promise<{
20
+ leaseId: string;
21
+ path: string;
22
+ branch: string;
23
+ expiresAt: number;
24
+ }>;
25
+ /**
26
+ * Slice 2026-07-29-worktree-l2-extended Part 12: container
27
+ * isolation bridge. Shells out to `peaks container spawn` to
28
+ * run `docker run` + write the container lease. Returns the
29
+ * leaseId the dispatch record needs to persist. The shape is
30
+ * a subset of the spawnWorktreeLease return (just leaseId);
31
+ * we do not need the path/branch/expiresAt for the container
32
+ * path because the envelope surfaces a different set of
33
+ * fields (image + containerId; see container-lease.ts).
34
+ */
35
+ export declare function spawnContainerLease(args: {
36
+ projectRoot: string;
37
+ sessionId: string;
38
+ rid: string;
39
+ role: string;
40
+ purpose: string;
41
+ }): Promise<{
42
+ leaseId: string;
43
+ }>;
@@ -0,0 +1,129 @@
1
+ import { spawn as childProcessSpawn } from 'node:child_process';
2
+ /**
3
+ * Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
4
+ * lease by shelling out to `peaks worktree spawn` (avoid re-implementing
5
+ * the lease-write + git-worktree-add sequence in this file). The CLI
6
+ * does the lease write, the git worktree add, AND the error handling;
7
+ * we just parse the JSON envelope and surface the leaseId + path.
8
+ *
9
+ * Throws on spawn failure; the caller converts the error to a
10
+ * ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
11
+ * dispatch is already async, the lease is a few hundred ms of FS work,
12
+ * and we need the leaseId before we build the dispatch record.
13
+ */
14
+ export function spawnWorktreeLease(args) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = childProcessSpawn(process.execPath, [
17
+ // The compiled CLI lives in dist/cli/peaks.js. We pass the entry
18
+ // through node so the test suite (which also runs on the same
19
+ // process) and the production binary share the same path. When
20
+ // the binary is invoked as `peaks`, the package bin stub does
21
+ // this for us; here we explicitly use process.execPath + the
22
+ // resolved entry to avoid PATH surprises.
23
+ process.argv[1] ?? '',
24
+ 'worktree', 'spawn',
25
+ '--rid', args.rid,
26
+ '--role', args.role,
27
+ '--purpose', args.purpose,
28
+ '--project', args.projectRoot,
29
+ '--session', args.sessionId,
30
+ '--json'
31
+ ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
32
+ let stdout = '';
33
+ let stderr = '';
34
+ child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
35
+ child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
36
+ child.on('error', (err) => reject(new Error(`worktree spawn subprocess failed: ${err.message}`)));
37
+ child.on('close', (code) => {
38
+ if (code !== 0) {
39
+ reject(new Error(`peaks worktree spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
40
+ return;
41
+ }
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(stdout);
45
+ }
46
+ catch (err) {
47
+ reject(new Error(`peaks worktree spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
48
+ return;
49
+ }
50
+ if (typeof parsed !== 'object' || parsed === null) {
51
+ reject(new Error('peaks worktree spawn envelope is not an object'));
52
+ return;
53
+ }
54
+ const env = parsed;
55
+ if (env.ok !== true || !env.data?.lease) {
56
+ reject(new Error(`peaks worktree spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
57
+ return;
58
+ }
59
+ resolve({
60
+ leaseId: env.data.lease.leaseId,
61
+ path: env.data.lease.path,
62
+ branch: env.data.lease.branch,
63
+ expiresAt: env.data.lease.expiresAt
64
+ });
65
+ // Part 47: unref via setImmediate so the close handler
66
+ // finishes first and Node's stdio 'end' events drain the
67
+ // stdout/stderr buffers before the parent releases the
68
+ // child handle. Without this microtask defer, the unref
69
+ // races the buffered stdout close and the test receives
70
+ // an empty JSON envelope.
71
+ setImmediate(() => { child.unref(); });
72
+ });
73
+ });
74
+ }
75
+ /**
76
+ * Slice 2026-07-29-worktree-l2-extended Part 12: container
77
+ * isolation bridge. Shells out to `peaks container spawn` to
78
+ * run `docker run` + write the container lease. Returns the
79
+ * leaseId the dispatch record needs to persist. The shape is
80
+ * a subset of the spawnWorktreeLease return (just leaseId);
81
+ * we do not need the path/branch/expiresAt for the container
82
+ * path because the envelope surfaces a different set of
83
+ * fields (image + containerId; see container-lease.ts).
84
+ */
85
+ export function spawnContainerLease(args) {
86
+ return new Promise((resolve, reject) => {
87
+ const child = childProcessSpawn(process.execPath, [
88
+ process.argv[1] ?? '',
89
+ 'container', 'spawn',
90
+ '--rid', args.rid,
91
+ '--role', args.role,
92
+ '--purpose', args.purpose,
93
+ '--project', args.projectRoot,
94
+ '--session', args.sessionId,
95
+ '--json'
96
+ ], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
97
+ let stdout = '';
98
+ let stderr = '';
99
+ child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
100
+ child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
101
+ child.on('error', (err) => reject(new Error(`container spawn subprocess failed: ${err.message}`)));
102
+ child.on('close', (code) => {
103
+ if (code !== 0) {
104
+ reject(new Error(`peaks container spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
105
+ return;
106
+ }
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(stdout);
110
+ }
111
+ catch (err) {
112
+ reject(new Error(`peaks container spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
113
+ return;
114
+ }
115
+ if (typeof parsed !== 'object' || parsed === null) {
116
+ reject(new Error('peaks container spawn envelope is not an object'));
117
+ return;
118
+ }
119
+ const env = parsed;
120
+ if (env.ok !== true || !env.data?.lease) {
121
+ reject(new Error(`peaks container spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
122
+ return;
123
+ }
124
+ resolve({ leaseId: env.data.lease.leaseId });
125
+ });
126
+ // See spawnWorktreeLease above for the rationale.
127
+ child.unref();
128
+ });
129
+ }
@@ -0,0 +1,26 @@
1
+ export type EvidenceGenerateOptions = {
2
+ projectRoot: string;
3
+ rid: string;
4
+ title: string;
5
+ files: string[];
6
+ lineCounts: Record<string, string>;
7
+ sessionId: string;
8
+ };
9
+ export type EvidenceGenerateResult = {
10
+ rid: string;
11
+ sessionId: string;
12
+ sessionRoot: string;
13
+ handoffPath: string;
14
+ handoffHash: string;
15
+ writtenFiles: string[];
16
+ createdDirectories: string[];
17
+ };
18
+ /** Comma/space tolerant `--files` splitter. */
19
+ export declare function parseFiles(raw: string): string[];
20
+ /**
21
+ * Parse `--line-counts "f=n;g=m"`. Accepts both `;` and `,` separators so
22
+ * the reference prototype's comma form and the task's semicolon form both
23
+ * work. `key=value` pairs without `=` are ignored (mirrors the prototype).
24
+ */
25
+ export declare function parseLineCounts(raw: string): Record<string, string>;
26
+ export declare function generateEvidence(options: EvidenceGenerateOptions): Promise<EvidenceGenerateResult>;