peaks-loop 4.0.32 → 4.0.34
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 +39 -0
- package/README-en.md +3 -4
- package/README.md +20 -28
- package/dist/cli/commands/_register.js +4 -3
- package/dist/cli/commands/audit-commands.js +12 -39
- package/dist/cli/commands/capability-commands.d.ts +6 -0
- package/dist/cli/commands/capability-commands.js +20 -2
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -3
- package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
- package/dist/cli/commands/code-mode-status-command.js +51 -0
- package/dist/cli/commands/code-run-command.js +8 -1
- package/dist/cli/commands/code-runtime-commands.js +13 -5
- package/dist/cli/commands/core/skill-command.js +1 -1
- package/dist/cli/commands/ecc-commands.js +3 -2
- package/dist/cli/commands/project-commands.js +1 -1
- package/dist/cli/commands/request-commands.js +3 -7
- package/dist/cli/commands/session-24h-mode.js +9 -1
- package/dist/cli/commands/shadcn-commands.d.ts +32 -0
- package/dist/cli/commands/shadcn-commands.js +120 -0
- package/dist/cli/commands/slice-commands.js +1 -4
- package/dist/services/24h-mode/auto-engage.d.ts +39 -0
- package/dist/services/24h-mode/auto-engage.js +46 -0
- package/dist/services/24h-mode/decider.d.ts +11 -5
- package/dist/services/24h-mode/decider.js +13 -13
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
- package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
- package/dist/services/audit/static-service.d.ts +1 -1
- package/dist/services/audit/static-service.js +1 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
- package/dist/services/code/auto-compact-lifecycle.js +1 -1
- package/dist/services/code/auto-compact-modes.d.ts +1 -1
- package/dist/services/code/auto-compact-modes.js +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +18 -22
- package/dist/services/code/mode-gate.d.ts +6 -2
- package/dist/services/code/mode-gate.js +9 -5
- package/dist/services/code/orchestrator-can-do.js +2 -2
- package/dist/services/code-review/ecc-bridge.d.ts +94 -15
- package/dist/services/code-review/ecc-bridge.js +133 -13
- package/dist/services/config/config-service.d.ts +17 -0
- package/dist/services/config/config-service.js +34 -0
- package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
- package/dist/services/context/auto-compact-dispatcher.js +4 -4
- package/dist/services/context/auto-compact-reader.js +8 -1
- package/dist/services/context/auto-compact-types.d.ts +13 -1
- package/dist/services/context/build-dispatch-system-prompt.js +2 -2
- package/dist/services/context/context-schema.d.ts +1 -1
- package/dist/services/dashboard/project-dashboard-service.d.ts +0 -7
- package/dist/services/dashboard/project-dashboard-service.js +1 -9
- package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
- package/dist/services/hooks/auto-compact-hook-install.js +2 -2
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
- package/dist/services/ide/ide-types.d.ts +10 -1
- package/dist/services/mode/mode-enforcement.d.ts +4 -1
- package/dist/services/mode/mode-enforcement.js +26 -28
- package/dist/services/mode/mode-status-service.d.ts +81 -0
- package/dist/services/mode/mode-status-service.js +108 -0
- package/dist/services/preferences/preferences-types.d.ts +16 -7
- package/dist/services/recommendations/capability-seed-items.js +0 -1
- package/dist/services/recommendations/installed-capability-detector.d.ts +33 -0
- package/dist/services/recommendations/installed-capability-detector.js +76 -0
- package/dist/services/scan/orphan-service.js +1 -1
- package/dist/services/skills/presence-lease-service.d.ts +21 -0
- package/dist/services/skills/presence-lease-service.js +26 -0
- package/dist/services/skills/skill-presence-service.d.ts +10 -1
- package/dist/services/skills/skill-presence-service.js +32 -4
- package/dist/services/skills/skill-statusline-service.js +6 -3
- package/dist/services/slice/multi-pass-orchestrator.js +0 -13
- package/dist/services/slice/slice-decompose-import-edges.d.ts +2 -2
- package/dist/services/slice/slice-decompose-import-edges.js +1 -29
- package/dist/services/slice/slice-decompose-runners.d.ts +2 -4
- package/dist/services/slice/slice-decompose-runners.js +1 -23
- package/dist/services/slice/slice-decompose-service.d.ts +3 -4
- package/dist/services/slice/slice-decompose-service.js +10 -29
- package/dist/services/slice/slice-decompose-types.d.ts +6 -47
- package/dist/services/slice/slice-decompose-types.js +3 -2
- package/dist/services/slice/slice-topology-types.d.ts +2 -3
- package/package.json +5 -5
- package/schemas/decomposition-v1.json +0 -12
- package/schemas/decomposition-v2.json +1 -15
- package/skills/bee/peaks-prd/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/external-references.md +1 -3
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -2
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +22 -8
- package/skills/bee/peaks-rd/references/rd-runbook.md +0 -2
- package/skills/bee/peaks-txt/SKILL.md +0 -12
- package/skills/peaks-code/SKILL.md +9 -9
- package/skills/peaks-code/references/mode-selection.md +7 -3
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/runbook.md +10 -5
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
- package/skills/peaks-code/references/standards-preflight.md +1 -1
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +0 -1
- package/skills/peaks-slice-decompose/references/v2-schema.md +1 -12
- package/dist/cli/commands/understand-commands.d.ts +0 -3
- package/dist/cli/commands/understand-commands.js +0 -148
- package/dist/services/understand/hybrid-types.d.ts +0 -32
- package/dist/services/understand/hybrid-types.js +0 -1
- package/dist/services/understand/understand-hybrid-service.d.ts +0 -10
- package/dist/services/understand/understand-hybrid-service.js +0 -100
- package/dist/services/understand/understand-scan-service.d.ts +0 -28
- package/dist/services/understand/understand-scan-service.js +0 -170
- package/dist/services/understand/understand-types.d.ts +0 -50
- package/dist/services/understand/understand-types.js +0 -1
|
@@ -48,7 +48,7 @@ Return a compact JSON envelope — do not write prose.
|
|
|
48
48
|
- session-id: <sid> (from .peaks/.active-skill.json or .peaks/.session.json)
|
|
49
49
|
- request-id: <rid> (PRD id)
|
|
50
50
|
- type: <type> (feature | bugfix | refactor | config | docs | chore)
|
|
51
|
-
- mode: <mode> (full-auto |
|
|
51
|
+
- mode: <mode> (full-auto | assisted | strict | 24h)
|
|
52
52
|
- project-scan-path: <path> (read this for component library / CSS / build tool)
|
|
53
53
|
- existing-system-path: <path> (legacy projects only)
|
|
54
54
|
- frontendOnly: <bool> (from project-scan)
|
|
@@ -209,7 +209,7 @@ Sub-agent presence in this list = Code launched a Task for it. Absence = the rol
|
|
|
209
209
|
| Mode | How the swarm plan is decided | What Code does |
|
|
210
210
|
|---|---|---|
|
|
211
211
|
| `full-auto` | Compute plan from signals above, no question to user | Auto-launch all sub-agents in the plan in parallel |
|
|
212
|
-
| `
|
|
212
|
+
| `24h` | Same as `full-auto` | Same as `full-auto` (long-run profile; the fan-out shape is identical, only the auto-compact cadence differs) |
|
|
213
213
|
| `assisted` | `AskUserQuestion` with three options: (a) Full — UI + RD(planning) + QA(test-cases); (b) Backend-only — RD(planning) + QA(test-cases); (c) Sequential — run RD first, then QA, skip UI | Use the user's choice as the plan |
|
|
214
214
|
| `strict` | Same as `assisted` (the question is informational; strict still enforces confirmation gates later) | Same as `assisted` |
|
|
215
215
|
|
|
@@ -182,5 +182,5 @@ ls <repo>/.claude/rules/common/coding-style.md \
|
|
|
182
182
|
# standards init/update with --apply and re-verify.
|
|
183
183
|
# Skipping Peaks-Loop Gate G (e.g. because the user did not explicitly authorize writes) is
|
|
184
184
|
# only acceptable in `assisted`/`strict` modes where the user actively declined; in
|
|
185
|
-
# `full-auto`/`
|
|
185
|
+
# `full-auto`/`24h` the absence of these files is a workflow violation.
|
|
186
186
|
```
|
|
@@ -50,7 +50,7 @@ If a doctor finding requires a code change, the workflow hands off to `peaks-rd`
|
|
|
50
50
|
## Boundaries
|
|
51
51
|
|
|
52
52
|
- The doctor is read-only. It does NOT modify code, fix bugs, or clean up sessions.
|
|
53
|
-
- The doctor does NOT install
|
|
53
|
+
- The doctor does NOT install any third-party tool. It just reports state.
|
|
54
54
|
- The doctor does NOT generate change records automatically; it surfaces findings + the LLM calls `peaks openspec from-doctor` to generate them.
|
|
55
55
|
|
|
56
56
|
## References
|
|
@@ -84,7 +84,6 @@ The envelope is a `DecompositionResultV2` (see `references/v2-schema.md`):
|
|
|
84
84
|
| `crossPassEdges` | Edges spanning passes (see `references/cross-pass-edge-interpretation.md`). |
|
|
85
85
|
| `llmArbitrations` | Trace of every LLM call made during decomposition (capped at 2 per invocation). |
|
|
86
86
|
| `codegraph` | `CodegraphEnvelope` — nodes/edges/dbMB/freshness/affectedCrossFile/note. |
|
|
87
|
-
| `understandAnything` | `UnderstandAnythingEnvelope` — kgNodes/kgEdges/available/fallback/note. |
|
|
88
87
|
| `partial` | `true` iff any pass failed to complete. |
|
|
89
88
|
|
|
90
89
|
The file is pretty-printed (`JSON.stringify(result, null, 2)`). Size scales with `passes.length * slices.length` plus the arbitration trace; typical runs are 5–80 KB.
|
|
@@ -21,7 +21,6 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
|
|
|
21
21
|
| `crossPassEdges` | `readonly CrossPassEdge[]` | `cross-pass-edge-merger.ts` (via `merge`) | Empty when `opts.llmRunner` is not provided. |
|
|
22
22
|
| `llmArbitrations` | `readonly LlmArbitration[]` | `merge.llmCalls` mapped via `llmCallsToArbitrations` | Empty when no LLM call was made. Cap: ≤ 2 calls per invocation. |
|
|
23
23
|
| `codegraph` | `CodegraphEnvelope` | from Pass 1 / Pass 2 result | Reused from v1 (see `slice-decompose-types.ts`). Falls back to `zeroCodegraph()` if neither pass ran. |
|
|
24
|
-
| `understandAnything` | `UnderstandAnythingEnvelope` | from Pass 1 / Pass 2 result | Reused from v1. Falls back to `zeroUnderstand()` if neither pass ran. |
|
|
25
24
|
| `partial` | `boolean` | orchestrator | `true` iff any pass failed to complete. Currently always `false` from `MultiPassOrchestrator`; reserved for future partial-failure paths. |
|
|
26
25
|
|
|
27
26
|
## `CodegraphEnvelope` (re-exported from v1)
|
|
@@ -35,16 +34,6 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
|
|
|
35
34
|
| `affectedCrossFile` | `boolean` | `true` iff `codegraph.affected` reported > 0 cross-file dependents. |
|
|
36
35
|
| `note` | `string` | Free-form operator note; `'no decomposition run'` when both passes were skipped. |
|
|
37
36
|
|
|
38
|
-
## `UnderstandAnythingEnvelope` (re-exported from v1)
|
|
39
|
-
|
|
40
|
-
| Field | Type | Notes |
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| `kgNodes` | `number` | Knowledge-graph node count. |
|
|
43
|
-
| `kgEdges` | `number` | Knowledge-graph edge count. |
|
|
44
|
-
| `available` | `boolean` | `false` when the `.understand-anything/knowledge-graph.json` file is missing. |
|
|
45
|
-
| `fallback` | `'semantic' \| 'structural-only'` | `'structural-only'` when `available` is `false`. |
|
|
46
|
-
| `note` | `string` | Free-form note. |
|
|
47
|
-
|
|
48
37
|
## `PassResult`
|
|
49
38
|
|
|
50
39
|
| Field | Type | Notes |
|
|
@@ -64,7 +53,7 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
|
|
|
64
53
|
| `files` | `readonly string[]` | Project-relative paths. |
|
|
65
54
|
| `loc` | `number` | Total LoC across all `files`. |
|
|
66
55
|
| `parentSliceId` | `string \| null` | `null` for Pass 1. For Pass 2, the id of the parent Pass 1 slice. |
|
|
67
|
-
| `semanticAnchor` | `string` |
|
|
56
|
+
| `semanticAnchor` | `string` | Always `"file:<wu.filePath>"` from the orchestrator's `workUnitToSliceV2` (codegraph / structural evidence). |
|
|
68
57
|
|
|
69
58
|
### `InternalEdge` (reserved)
|
|
70
59
|
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { InvalidArgumentError } from 'commander';
|
|
2
|
-
import { scanUnderstandAnything, summarizeKnowledgeGraph } from '../../services/understand/understand-scan-service.js';
|
|
3
|
-
import { buildUnderstandContext } from '../../services/understand/understand-hybrid-service.js';
|
|
4
|
-
import { fail, ok } from 'peaks-loop-shared/result';
|
|
5
|
-
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
6
|
-
function parsePositiveInteger(value) {
|
|
7
|
-
if (!/^\d+$/.test(value)) {
|
|
8
|
-
throw new InvalidArgumentError('must be a positive integer');
|
|
9
|
-
}
|
|
10
|
-
const parsed = Number(value);
|
|
11
|
-
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
12
|
-
throw new InvalidArgumentError('must be a positive integer');
|
|
13
|
-
}
|
|
14
|
-
return parsed;
|
|
15
|
-
}
|
|
16
|
-
const INSTALL_HINT = 'Install Understand Anything in Claude Code: `/plugin marketplace add Lum1104/Understand-Anything` then `/plugin install understand-anything`, then run `/understand` in the target project to generate .understand-anything/knowledge-graph.json.';
|
|
17
|
-
export function registerUnderstandCommands(program, io) {
|
|
18
|
-
const understand = program.command('understand').description('Inspect Understand Anything artifacts inside a project (read-only)');
|
|
19
|
-
addJsonOption(understand
|
|
20
|
-
.command('status')
|
|
21
|
-
.description('Report whether Understand Anything has produced a knowledge graph in the target project')
|
|
22
|
-
.requiredOption('--project <path>', 'target project root')
|
|
23
|
-
.option('--artifact-dir <path>', 'override the default .understand-anything directory')).action(async (options) => {
|
|
24
|
-
try {
|
|
25
|
-
const scanOptions = { projectRoot: options.project };
|
|
26
|
-
if (options.artifactDir !== undefined) {
|
|
27
|
-
scanOptions.artifactDir = options.artifactDir;
|
|
28
|
-
}
|
|
29
|
-
const report = await scanUnderstandAnything(scanOptions);
|
|
30
|
-
const nextActions = [];
|
|
31
|
-
if (!report.exists) {
|
|
32
|
-
nextActions.push(INSTALL_HINT);
|
|
33
|
-
}
|
|
34
|
-
else if (!report.graph.exists) {
|
|
35
|
-
nextActions.push('Run `/understand` inside Claude Code on this project to generate .understand-anything/knowledge-graph.json.');
|
|
36
|
-
}
|
|
37
|
-
else if (report.graph.parseError !== undefined) {
|
|
38
|
-
nextActions.push('Re-run `/understand` to regenerate the knowledge graph; the current file is not valid JSON.');
|
|
39
|
-
}
|
|
40
|
-
printResult(io, ok('understand.status', report, [], nextActions), options.json);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
printResult(io, fail('understand.status', 'UNDERSTAND_STATUS_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
|
|
44
|
-
process.exitCode = 1;
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
addJsonOption(understand
|
|
48
|
-
.command('show')
|
|
49
|
-
.description('Summarize the Understand Anything knowledge graph (counts, layers, tours, sample nodes) for RD/TXT consumption')
|
|
50
|
-
.requiredOption('--project <path>', 'target project root')
|
|
51
|
-
.option('--artifact-dir <path>', 'override the default .understand-anything directory')
|
|
52
|
-
.option('--sample <n>', 'maximum number of sample node ids to return (default 5)', parsePositiveInteger)).action(async (options) => {
|
|
53
|
-
try {
|
|
54
|
-
const summaryOptions = { projectRoot: options.project };
|
|
55
|
-
if (options.artifactDir !== undefined) {
|
|
56
|
-
summaryOptions.artifactDir = options.artifactDir;
|
|
57
|
-
}
|
|
58
|
-
if (options.sample !== undefined) {
|
|
59
|
-
summaryOptions.sampleSize = options.sample;
|
|
60
|
-
}
|
|
61
|
-
const summary = await summarizeKnowledgeGraph(summaryOptions);
|
|
62
|
-
if (!summary.exists) {
|
|
63
|
-
printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_MISSING', `No knowledge graph found at ${summary.path}`, summary, [INSTALL_HINT]), options.json);
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (summary.parseError !== undefined) {
|
|
68
|
-
printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_PARSE_ERROR', `Knowledge graph at ${summary.path} is not valid JSON: ${summary.parseError}`, summary, ['Re-run `/understand` to regenerate the knowledge graph']), options.json);
|
|
69
|
-
process.exitCode = 1;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
printResult(io, ok('understand.show', summary), options.json);
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
printResult(io, fail('understand.show', 'UNDERSTAND_SHOW_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
|
|
76
|
-
process.exitCode = 1;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
// L3.1: opt-in UX subcommand. Returns the AskUserQuestion payload that
|
|
80
|
-
// the LLM-side UX layer (peaks-code / peaks-ide) should surface when
|
|
81
|
-
// uaPrompt === 'unset' and UA is absent. When uaPrompt is skip-this-session
|
|
82
|
-
// or skip-forever, returns a no-op envelope (caller does not prompt).
|
|
83
|
-
addJsonOption(understand
|
|
84
|
-
.command('opt-in')
|
|
85
|
-
.description('Returns the UA opt-in prompt payload (Slice L3.1) when uaPrompt is unset; no-op otherwise')
|
|
86
|
-
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
87
|
-
try {
|
|
88
|
-
const report = await scanUnderstandAnything({ projectRoot: options.project });
|
|
89
|
-
const uaPrompt = report.uaPrompt ?? 'unset';
|
|
90
|
-
if (report.exists || uaPrompt !== 'unset') {
|
|
91
|
-
// No prompt needed: UA is installed OR user already decided.
|
|
92
|
-
printResult(io, ok('understand.opt-in', { promptNeeded: false, uaPrompt, uaInstalled: report.exists }), options.json);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const prompt = {
|
|
96
|
-
version: 1,
|
|
97
|
-
tool: 'ua-opt-in',
|
|
98
|
-
artifactDir: report.artifactDir,
|
|
99
|
-
reason: 'ua-artifact-missing',
|
|
100
|
-
options: [
|
|
101
|
-
{ id: 'install', label: 'Install UA in Claude Code', description: INSTALL_HINT },
|
|
102
|
-
{ id: 'fallback-this-session', label: 'Use codegraph fallback this session', description: 'Skip UA this run; do not write preferences.json' },
|
|
103
|
-
{ id: 'fallback-forever', label: 'Use codegraph fallback forever', description: 'Write preferences.json:uaPrompt=skip-forever; suppress future prompts' }
|
|
104
|
-
]
|
|
105
|
-
};
|
|
106
|
-
printResult(io, ok('understand.opt-in', { promptNeeded: true, uaPrompt, uaInstalled: false, prompt }), options.json);
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
printResult(io, fail('understand.opt-in', 'UNDERSTAND_OPTIN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
|
|
110
|
-
process.exitCode = 1;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
// Slice 2026-07-02-codegraph-ua-hybrid: hybrid context subcommand.
|
|
114
|
-
// Routes UA-first / codegraph-fallback / hybrid in the service layer;
|
|
115
|
-
// the CLI is a thin shell that surfaces the envelope and exit code.
|
|
116
|
-
addJsonOption(understand
|
|
117
|
-
.command('context')
|
|
118
|
-
.description('Build a hybrid project-context envelope: UA knowledge graph when present, codegraph affected as fallback, or both in parallel')
|
|
119
|
-
.requiredOption('--project <path>', 'target project root')
|
|
120
|
-
.option('--artifact-dir <path>', 'override the default .understand-anything directory')
|
|
121
|
-
.option('--sample <n>', 'maximum number of sample node ids to include (default 5)', parsePositiveInteger)
|
|
122
|
-
.option('--files <file...>', 'file globs to feed `codegraph affected` (default: src/index.ts, package.json, README.md)')).action(async (options) => {
|
|
123
|
-
try {
|
|
124
|
-
const ctxOptions = { projectRoot: options.project };
|
|
125
|
-
if (options.artifactDir !== undefined)
|
|
126
|
-
ctxOptions.artifactDir = options.artifactDir;
|
|
127
|
-
if (options.sample !== undefined)
|
|
128
|
-
ctxOptions.sampleSize = options.sample;
|
|
129
|
-
if (options.files !== undefined)
|
|
130
|
-
ctxOptions.files = options.files;
|
|
131
|
-
const result = await buildUnderstandContext(ctxOptions);
|
|
132
|
-
const exitCode = result.source === 'both-missing' ? 2 : 0;
|
|
133
|
-
if (exitCode !== 0) {
|
|
134
|
-
printResult(io, fail('understand.context', 'UNDERSTAND_CONTEXT_NO_EVIDENCE', `No UA graph and codegraph affected produced no usable evidence`, result, [
|
|
135
|
-
INSTALL_HINT,
|
|
136
|
-
'Run `peaks codegraph index` in this project to populate the codegraph index, then retry'
|
|
137
|
-
]), options.json);
|
|
138
|
-
process.exitCode = exitCode;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
printResult(io, ok('understand.context', result, [], result.warnings.length > 0 ? result.warnings : undefined), options.json);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
printResult(io, fail('understand.context', 'UNDERSTAND_CONTEXT_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and codegraph installation before retrying']), options.json);
|
|
145
|
-
process.exitCode = 1;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { CodegraphExecutionResult } from '../codegraph/codegraph-service.js';
|
|
2
|
-
import type { UnderstandScanReport } from './understand-types.js';
|
|
3
|
-
import type { UnderstandGraphSummary } from './understand-scan-service.js';
|
|
4
|
-
/**
|
|
5
|
-
* Slice 2026-07-02-codegraph-ua-hybrid — envelope contract for the
|
|
6
|
-
* `peaks understand context` command. The service layer decides which
|
|
7
|
-
* evidence to include based on environment detection; the source field
|
|
8
|
-
* is the authoritative routing signal.
|
|
9
|
-
*/
|
|
10
|
-
export type UnderstandContextSource = 'ua-only' | 'ua-missing-fallback-codegraph' | 'ua-and-codegraph-hybrid' | 'both-missing';
|
|
11
|
-
export type CodegraphContextBlock = {
|
|
12
|
-
invocation: {
|
|
13
|
-
subcommand: 'affected';
|
|
14
|
-
files: string[];
|
|
15
|
-
};
|
|
16
|
-
execution: CodegraphExecutionResult;
|
|
17
|
-
/** Parsed JSON from `codegraph affected --json` when exitCode === 0; null otherwise. */
|
|
18
|
-
payload: unknown;
|
|
19
|
-
parseError?: string;
|
|
20
|
-
};
|
|
21
|
-
export type UnderstandContextResult = {
|
|
22
|
-
projectRoot: string;
|
|
23
|
-
source: UnderstandContextSource;
|
|
24
|
-
ua?: {
|
|
25
|
-
scan: UnderstandScanReport;
|
|
26
|
-
summary?: UnderstandGraphSummary;
|
|
27
|
-
};
|
|
28
|
-
codegraph?: CodegraphContextBlock;
|
|
29
|
-
/** Wall-clock milliseconds spent on the slowest parallel branch. */
|
|
30
|
-
durationMs: number;
|
|
31
|
-
warnings: string[];
|
|
32
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type CodegraphProcessRunner } from '../codegraph/codegraph-service.js';
|
|
2
|
-
import type { UnderstandContextResult } from './hybrid-types.js';
|
|
3
|
-
export type BuildUnderstandContextOptions = {
|
|
4
|
-
projectRoot: string;
|
|
5
|
-
files?: string[];
|
|
6
|
-
sampleSize?: number;
|
|
7
|
-
artifactDir?: string;
|
|
8
|
-
codegraphRunner?: CodegraphProcessRunner;
|
|
9
|
-
};
|
|
10
|
-
export declare function buildUnderstandContext(options: BuildUnderstandContextOptions): Promise<UnderstandContextResult>;
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { createCodegraphInvocation, executeCodegraphInvocation } from '../codegraph/codegraph-service.js';
|
|
2
|
-
import { scanUnderstandAnything, summarizeKnowledgeGraph } from './understand-scan-service.js';
|
|
3
|
-
import { getErrorMessage } from 'peaks-loop-shared/result';
|
|
4
|
-
const DEFAULT_FILES = ['src/index.ts', 'package.json', 'README.md'];
|
|
5
|
-
function resolveFiles(files) {
|
|
6
|
-
if (files !== undefined && files.length > 0) {
|
|
7
|
-
return [...files];
|
|
8
|
-
}
|
|
9
|
-
return [...DEFAULT_FILES];
|
|
10
|
-
}
|
|
11
|
-
async function safeSummarize(options) {
|
|
12
|
-
try {
|
|
13
|
-
return { summary: await summarizeKnowledgeGraph(options) };
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
return { warning: `UA summary failed: ${getErrorMessage(error)}` };
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async function runCodegraph(projectRoot, files, runner) {
|
|
20
|
-
try {
|
|
21
|
-
const invocation = createCodegraphInvocation({
|
|
22
|
-
subcommand: 'affected',
|
|
23
|
-
project: projectRoot,
|
|
24
|
-
files,
|
|
25
|
-
json: true
|
|
26
|
-
});
|
|
27
|
-
const execution = await executeCodegraphInvocation(invocation, runner);
|
|
28
|
-
if (execution.exitCode !== 0) {
|
|
29
|
-
return { warning: `codegraph affected exited with code ${execution.exitCode}: ${execution.stderr.slice(0, 200)}` };
|
|
30
|
-
}
|
|
31
|
-
let payload = null;
|
|
32
|
-
let parseError;
|
|
33
|
-
try {
|
|
34
|
-
payload = JSON.parse(execution.stdout);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
parseError = getErrorMessage(error);
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
block: {
|
|
41
|
-
invocation: { subcommand: 'affected', files },
|
|
42
|
-
execution,
|
|
43
|
-
payload,
|
|
44
|
-
...(parseError !== undefined ? { parseError } : {})
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
return { warning: `codegraph affected failed: ${getErrorMessage(error)}` };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function decideSource(uaScan, hasCodegraph) {
|
|
53
|
-
const uaPresent = uaScan.exists && uaScan.graph.exists && uaScan.graph.parseError === undefined;
|
|
54
|
-
if (uaPresent && hasCodegraph)
|
|
55
|
-
return 'ua-and-codegraph-hybrid';
|
|
56
|
-
if (uaPresent)
|
|
57
|
-
return 'ua-only';
|
|
58
|
-
if (hasCodegraph)
|
|
59
|
-
return 'ua-missing-fallback-codegraph';
|
|
60
|
-
return 'both-missing';
|
|
61
|
-
}
|
|
62
|
-
export async function buildUnderstandContext(options) {
|
|
63
|
-
const startedAt = Date.now();
|
|
64
|
-
const files = resolveFiles(options.files);
|
|
65
|
-
const sampleSize = options.sampleSize ?? 5;
|
|
66
|
-
const warnings = [];
|
|
67
|
-
const scanOptions = { projectRoot: options.projectRoot };
|
|
68
|
-
if (options.artifactDir !== undefined) {
|
|
69
|
-
scanOptions.artifactDir = options.artifactDir;
|
|
70
|
-
}
|
|
71
|
-
const uaScan = await scanUnderstandAnything(scanOptions);
|
|
72
|
-
const uaSummaryPromise = uaScan.exists && uaScan.graph.exists
|
|
73
|
-
? safeSummarize({ projectRoot: options.projectRoot, sampleSize, ...(options.artifactDir !== undefined ? { artifactDir: options.artifactDir } : {}) })
|
|
74
|
-
: Promise.resolve({});
|
|
75
|
-
const [uaSummary, codegraphResult] = await Promise.all([
|
|
76
|
-
uaSummaryPromise,
|
|
77
|
-
runCodegraph(options.projectRoot, files, options.codegraphRunner)
|
|
78
|
-
]);
|
|
79
|
-
if (uaSummary.warning)
|
|
80
|
-
warnings.push(uaSummary.warning);
|
|
81
|
-
if (codegraphResult.warning)
|
|
82
|
-
warnings.push(codegraphResult.warning);
|
|
83
|
-
const source = decideSource(uaScan, codegraphResult.block !== undefined);
|
|
84
|
-
const result = {
|
|
85
|
-
projectRoot: options.projectRoot,
|
|
86
|
-
source,
|
|
87
|
-
durationMs: Date.now() - startedAt,
|
|
88
|
-
warnings
|
|
89
|
-
};
|
|
90
|
-
if (source === 'ua-only' || source === 'ua-and-codegraph-hybrid') {
|
|
91
|
-
result.ua = {
|
|
92
|
-
scan: uaScan,
|
|
93
|
-
...(uaSummary.summary !== undefined ? { summary: uaSummary.summary } : {})
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
if (codegraphResult.block) {
|
|
97
|
-
result.codegraph = codegraphResult.block;
|
|
98
|
-
}
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { UnderstandScanReport } from './understand-types.js';
|
|
2
|
-
export type UnderstandScanOptions = {
|
|
3
|
-
projectRoot: string;
|
|
4
|
-
artifactDir?: string;
|
|
5
|
-
};
|
|
6
|
-
export declare function scanUnderstandAnything(options: UnderstandScanOptions): Promise<UnderstandScanReport>;
|
|
7
|
-
export type UnderstandGraphSummary = {
|
|
8
|
-
exists: boolean;
|
|
9
|
-
path: string;
|
|
10
|
-
generatedAt: string | null;
|
|
11
|
-
topLevelFields: string[];
|
|
12
|
-
counts: {
|
|
13
|
-
nodes: number;
|
|
14
|
-
edges: number;
|
|
15
|
-
layers: number;
|
|
16
|
-
tours: number;
|
|
17
|
-
};
|
|
18
|
-
layerNames: string[];
|
|
19
|
-
tourNames: string[];
|
|
20
|
-
sampleNodes: string[];
|
|
21
|
-
parseError?: string;
|
|
22
|
-
};
|
|
23
|
-
export type SummarizeKnowledgeGraphOptions = {
|
|
24
|
-
projectRoot: string;
|
|
25
|
-
artifactDir?: string;
|
|
26
|
-
sampleSize?: number;
|
|
27
|
-
};
|
|
28
|
-
export declare function summarizeKnowledgeGraph(options: SummarizeKnowledgeGraphOptions): Promise<UnderstandGraphSummary>;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { isDirectory, pathExists, readText } from 'peaks-loop-shared/fs';
|
|
4
|
-
import { getErrorMessage } from 'peaks-loop-shared/result';
|
|
5
|
-
import { loadPreferences } from '../preferences/preferences-service.js';
|
|
6
|
-
function defaultArtifactDir(projectRoot) {
|
|
7
|
-
return join(projectRoot, '.understand-anything');
|
|
8
|
-
}
|
|
9
|
-
function countArrayField(record, key) {
|
|
10
|
-
const value = record[key];
|
|
11
|
-
return Array.isArray(value) ? value.length : 0;
|
|
12
|
-
}
|
|
13
|
-
async function readFlag(path) {
|
|
14
|
-
return { exists: await pathExists(path), path };
|
|
15
|
-
}
|
|
16
|
-
async function readGraph(graphPath) {
|
|
17
|
-
if (!(await pathExists(graphPath))) {
|
|
18
|
-
return { exists: false, path: graphPath };
|
|
19
|
-
}
|
|
20
|
-
const stats = await stat(graphPath);
|
|
21
|
-
const raw = await readText(graphPath);
|
|
22
|
-
try {
|
|
23
|
-
const parsed = JSON.parse(raw);
|
|
24
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
25
|
-
return {
|
|
26
|
-
exists: true,
|
|
27
|
-
path: graphPath,
|
|
28
|
-
sizeBytes: stats.size,
|
|
29
|
-
parseError: 'knowledge-graph.json must be a JSON object'
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
const record = parsed;
|
|
33
|
-
return {
|
|
34
|
-
exists: true,
|
|
35
|
-
path: graphPath,
|
|
36
|
-
sizeBytes: stats.size,
|
|
37
|
-
topLevelFields: Object.keys(record).sort(),
|
|
38
|
-
counts: {
|
|
39
|
-
nodes: countArrayField(record, 'nodes'),
|
|
40
|
-
edges: countArrayField(record, 'edges'),
|
|
41
|
-
layers: countArrayField(record, 'layers'),
|
|
42
|
-
tours: countArrayField(record, 'tours')
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
return {
|
|
48
|
-
exists: true,
|
|
49
|
-
path: graphPath,
|
|
50
|
-
sizeBytes: stats.size,
|
|
51
|
-
parseError: getErrorMessage(error)
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
export async function scanUnderstandAnything(options) {
|
|
56
|
-
const artifactDir = options.artifactDir ?? defaultArtifactDir(options.projectRoot);
|
|
57
|
-
const exists = await isDirectory(artifactDir);
|
|
58
|
-
// L3.1: read uaPrompt from preferences.json (graceful — returns 'unset' if missing/broken)
|
|
59
|
-
const uaPrompt = await readUaPrompt(options.projectRoot);
|
|
60
|
-
if (!exists) {
|
|
61
|
-
return {
|
|
62
|
-
exists: false,
|
|
63
|
-
artifactDir,
|
|
64
|
-
graph: { exists: false, path: join(artifactDir, 'knowledge-graph.json') },
|
|
65
|
-
intermediate: { exists: false, path: join(artifactDir, 'intermediate') },
|
|
66
|
-
diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') },
|
|
67
|
-
uaPrompt
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const graph = await readGraph(join(artifactDir, 'knowledge-graph.json'));
|
|
71
|
-
const intermediate = await readFlag(join(artifactDir, 'intermediate'));
|
|
72
|
-
const diffOverlay = await readFlag(join(artifactDir, 'diff-overlay.json'));
|
|
73
|
-
return { exists: true, artifactDir, graph, intermediate, diffOverlay, uaPrompt };
|
|
74
|
-
}
|
|
75
|
-
async function readUaPrompt(projectRoot) {
|
|
76
|
-
try {
|
|
77
|
-
const prefs = await loadPreferences(projectRoot);
|
|
78
|
-
return prefs.uaPrompt;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return 'unset';
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function pickStringId(value) {
|
|
85
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
const record = value;
|
|
89
|
-
for (const key of ['id', 'path', 'name', 'label']) {
|
|
90
|
-
const candidate = record[key];
|
|
91
|
-
if (typeof candidate === 'string' && candidate.length > 0) {
|
|
92
|
-
return candidate;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
function pickStringField(record, key) {
|
|
98
|
-
const value = record[key];
|
|
99
|
-
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
100
|
-
}
|
|
101
|
-
function pickNameArray(record, key) {
|
|
102
|
-
const value = record[key];
|
|
103
|
-
if (!Array.isArray(value)) {
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
return value
|
|
107
|
-
.map((entry) => {
|
|
108
|
-
if (typeof entry === 'string') {
|
|
109
|
-
return entry;
|
|
110
|
-
}
|
|
111
|
-
return pickStringId(entry);
|
|
112
|
-
})
|
|
113
|
-
.filter((entry) => entry !== null);
|
|
114
|
-
}
|
|
115
|
-
export async function summarizeKnowledgeGraph(options) {
|
|
116
|
-
const scanOptions = { projectRoot: options.projectRoot };
|
|
117
|
-
if (options.artifactDir !== undefined) {
|
|
118
|
-
scanOptions.artifactDir = options.artifactDir;
|
|
119
|
-
}
|
|
120
|
-
const scan = await scanUnderstandAnything(scanOptions);
|
|
121
|
-
const sampleSize = options.sampleSize ?? 5;
|
|
122
|
-
if (!scan.graph.exists) {
|
|
123
|
-
return {
|
|
124
|
-
exists: false,
|
|
125
|
-
path: scan.graph.path,
|
|
126
|
-
generatedAt: null,
|
|
127
|
-
topLevelFields: [],
|
|
128
|
-
counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
|
|
129
|
-
layerNames: [],
|
|
130
|
-
tourNames: [],
|
|
131
|
-
sampleNodes: []
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (scan.graph.parseError !== undefined) {
|
|
135
|
-
return {
|
|
136
|
-
exists: true,
|
|
137
|
-
path: scan.graph.path,
|
|
138
|
-
generatedAt: null,
|
|
139
|
-
topLevelFields: [],
|
|
140
|
-
counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
|
|
141
|
-
layerNames: [],
|
|
142
|
-
tourNames: [],
|
|
143
|
-
sampleNodes: [],
|
|
144
|
-
parseError: scan.graph.parseError
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
const raw = await readText(scan.graph.path);
|
|
148
|
-
const parsed = JSON.parse(raw);
|
|
149
|
-
const record = parsed;
|
|
150
|
-
const generatedAt = pickStringField(record, 'generatedAt');
|
|
151
|
-
const nodes = Array.isArray(record.nodes) ? record.nodes : [];
|
|
152
|
-
const sampleNodes = nodes.slice(0, sampleSize)
|
|
153
|
-
.map((entry) => pickStringId(entry))
|
|
154
|
-
.filter((entry) => entry !== null);
|
|
155
|
-
return {
|
|
156
|
-
exists: true,
|
|
157
|
-
path: scan.graph.path,
|
|
158
|
-
generatedAt,
|
|
159
|
-
topLevelFields: Object.keys(record).sort(),
|
|
160
|
-
counts: {
|
|
161
|
-
nodes: countArrayField(record, 'nodes'),
|
|
162
|
-
edges: countArrayField(record, 'edges'),
|
|
163
|
-
layers: countArrayField(record, 'layers'),
|
|
164
|
-
tours: countArrayField(record, 'tours')
|
|
165
|
-
},
|
|
166
|
-
layerNames: pickNameArray(record, 'layers'),
|
|
167
|
-
tourNames: pickNameArray(record, 'tours'),
|
|
168
|
-
sampleNodes
|
|
169
|
-
};
|
|
170
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export type UnderstandGraphReport = {
|
|
2
|
-
exists: boolean;
|
|
3
|
-
path: string;
|
|
4
|
-
sizeBytes?: number;
|
|
5
|
-
topLevelFields?: string[];
|
|
6
|
-
counts?: {
|
|
7
|
-
nodes: number;
|
|
8
|
-
edges: number;
|
|
9
|
-
layers: number;
|
|
10
|
-
tours: number;
|
|
11
|
-
};
|
|
12
|
-
parseError?: string;
|
|
13
|
-
};
|
|
14
|
-
export type UnderstandFlagReport = {
|
|
15
|
-
exists: boolean;
|
|
16
|
-
path: string;
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Slice L3.1 — UA opt-in UX state. 'unset' triggers an opt-in prompt on
|
|
20
|
-
* first scan; 'skip-this-session' suppresses the prompt for the current
|
|
21
|
-
* session; 'skip-forever' writes to .peaks/preferences.json to suppress
|
|
22
|
-
* all future prompts. Mirrors preferences.json:uaPrompt.
|
|
23
|
-
*/
|
|
24
|
-
export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
|
|
25
|
-
export type UnderstandScanReport = {
|
|
26
|
-
exists: boolean;
|
|
27
|
-
artifactDir: string;
|
|
28
|
-
graph: UnderstandGraphReport;
|
|
29
|
-
intermediate: UnderstandFlagReport;
|
|
30
|
-
diffOverlay: UnderstandFlagReport;
|
|
31
|
-
/** Slice L3.1: opt-in UX state from preferences.json:uaPrompt. */
|
|
32
|
-
readonly uaPrompt?: UaPromptDecision;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Slice L3.1 — opt-in prompt payload. When uaPrompt === 'unset' and UA is
|
|
36
|
-
* absent, the peaks-code / peaks-ide layer surfaces this to the user via
|
|
37
|
-
* AskUserQuestion. The CLI does not prompt directly; it returns this
|
|
38
|
-
* payload so the LLM-side UX layer can decide.
|
|
39
|
-
*/
|
|
40
|
-
export interface UaOptInPrompt {
|
|
41
|
-
readonly version: 1;
|
|
42
|
-
readonly tool: 'ua-opt-in';
|
|
43
|
-
readonly artifactDir: string;
|
|
44
|
-
readonly reason: 'ua-artifact-missing';
|
|
45
|
-
readonly options: readonly {
|
|
46
|
-
readonly id: 'install' | 'fallback-this-session' | 'fallback-forever';
|
|
47
|
-
readonly label: string;
|
|
48
|
-
readonly description: string;
|
|
49
|
-
}[];
|
|
50
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|