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
|
@@ -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
|
-
|
|
35
|
-
|
|
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>;
|