peaks-loop 4.0.16 → 4.0.18

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 (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +39 -26
  5. package/dist/cli/commands/_register.js +2 -1
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/cron-commands.js +16 -4
  12. package/dist/cli/commands/dashboard-long-run.js +8 -4
  13. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  14. package/dist/cli/commands/hooks-commands.js +56 -3
  15. package/dist/cli/commands/primer-command.d.ts +59 -0
  16. package/dist/cli/commands/primer-command.js +160 -0
  17. package/dist/cli/commands/qa-commands.js +1 -2
  18. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  19. package/dist/cli/commands/slice-commands.js +20 -8
  20. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  21. package/dist/cli/commands/statusline-commands.js +17 -3
  22. package/dist/cli/commands/tech-commands.d.ts +1 -1
  23. package/dist/cli/program.js +8 -0
  24. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  25. package/dist/services/artifacts/request-artifact-service.js +10 -1
  26. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  27. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  28. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  29. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  30. package/dist/services/code/orchestrator-can-do.js +48 -2
  31. package/dist/services/code/post-compact-detector.d.ts +1 -1
  32. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  33. package/dist/services/config/config-safety.d.ts +23 -0
  34. package/dist/services/config/config-safety.js +62 -0
  35. package/dist/services/config/config-service.d.ts +1 -1
  36. package/dist/services/config/config-service.js +2 -4
  37. package/dist/services/container/container-lease.js +21 -8
  38. package/dist/services/context/spillover-store.d.ts +1 -1
  39. package/dist/services/crystallization/crystallization-types.js +20 -8
  40. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  41. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  42. package/dist/services/evolution/evolution-types.js +38 -21
  43. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  44. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  45. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  46. package/dist/services/ide/current-model-detector.js +1 -2
  47. package/dist/services/lint/detect-eslint.js +8 -5
  48. package/dist/services/lint/eslint-runner.d.ts +1 -2
  49. package/dist/services/lint/eslint-runner.js +86 -23
  50. package/dist/services/lint/npx-resolver.d.ts +6 -0
  51. package/dist/services/lint/npx-resolver.js +47 -0
  52. package/dist/services/loop/loop-release-types.js +32 -13
  53. package/dist/services/loop/spec-service.d.ts +7 -1
  54. package/dist/services/loop/spec-service.js +218 -182
  55. package/dist/services/rd/rd-service.js +22 -9
  56. package/dist/services/scan/archetype-service.js +21 -9
  57. package/dist/services/session/binding-store.js +1 -2
  58. package/dist/services/share/bundle-reader.d.ts +6 -9
  59. package/dist/services/share/bundle-reader.js +297 -223
  60. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  61. package/dist/services/skills/hooks-settings-service.js +48 -3
  62. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  63. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  64. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  65. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  66. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  67. package/dist/services/skills/skill-statusline-service.js +56 -6
  68. package/dist/services/slice/calibration-store.js +21 -8
  69. package/dist/services/slice/slice-check-service.js +35 -14
  70. package/dist/services/slice/slice-decompose-service.js +459 -223
  71. package/dist/services/standards/project-context.js +135 -73
  72. package/dist/services/verdict/envelopes.d.ts +1 -2
  73. package/dist/services/verdict/envelopes.js +1 -2
  74. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  75. package/dist/services/vm/vm-lease.js +19 -8
  76. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  77. package/dist/services/worktree/worktree-lease.js +19 -8
  78. package/dist/shared/fs-utils.d.ts +1 -1
  79. package/package.json +7 -4
  80. package/skills/peaks-code/SKILL.md +46 -0
@@ -54,46 +54,83 @@ function majorOf(version) {
54
54
  const major = cleaned.split('.')[0];
55
55
  return major !== undefined && /^\d+$/.test(major) ? major : undefined;
56
56
  }
57
+ const LIBRARY_DETECTORS = [
58
+ detectAntd,
59
+ detectMui,
60
+ detectElementPlus,
61
+ detectElementUi,
62
+ detectArco,
63
+ detectTdesign,
64
+ detectSemi,
65
+ detectNextui,
66
+ detectChakra,
67
+ detectVant,
68
+ detectShadcn
69
+ ];
57
70
  export function detectComponentLibrary(projectRoot, deps) {
58
- if ('antd' in deps) {
59
- const major = majorOf(deps['antd']);
60
- const hasProSuite = '@ant-design/pro-components' in deps || Object.keys(deps).some((d) => d.startsWith('@ant-design/pro-'));
61
- return hasProSuite
62
- ? { name: 'antd-pro', ...(major !== undefined ? { majorVersion: major } : {}), hasProSuite: true }
63
- : { name: 'antd', ...(major !== undefined ? { majorVersion: major } : {}) };
71
+ for (const detector of LIBRARY_DETECTORS) {
72
+ const result = detector(deps, projectRoot);
73
+ if (result !== null)
74
+ return result;
64
75
  }
65
- if ('@mui/material' in deps)
66
- return { name: 'mui', ...(majorOf(deps['@mui/material']) !== undefined ? { majorVersion: majorOf(deps['@mui/material']) } : {}) };
67
- if ('element-plus' in deps)
68
- return { name: 'element-plus' };
69
- if ('element-ui' in deps)
70
- return { name: 'element-ui' };
71
- if ('@arco-design/web-react' in deps)
72
- return { name: 'arco' };
73
- if ('tdesign-react' in deps || 'tdesign-vue-next' in deps)
74
- return { name: 'tdesign' };
75
- if ('@douyinfe/semi-ui' in deps)
76
- return { name: 'semi' };
77
- if ('@nextui-org/react' in deps)
78
- return { name: 'nextui' };
79
- if ('@chakra-ui/react' in deps)
80
- return { name: 'chakra' };
81
- if ('vant' in deps)
82
- return { name: 'vant' };
83
- // shadcn / ui is a Tailwind-based component primitive set shipped as
84
- // copyable source files under `components/ui/`. It has no top-level
85
- // package of its own, so the signal we use is: Tailwind is present
86
- // AND at least one of its peer utilities is also present.
87
- if (('class-variance-authority' in deps
76
+ return { name: 'none' };
77
+ }
78
+ function detectAntd(deps, _projectRoot) {
79
+ if (!('antd' in deps))
80
+ return null;
81
+ const major = majorOf(deps['antd']);
82
+ const hasProSuite = '@ant-design/pro-components' in deps
83
+ || Object.keys(deps).some((d) => d.startsWith('@ant-design/pro-'));
84
+ return hasProSuite
85
+ ? { name: 'antd-pro', ...(major !== undefined ? { majorVersion: major } : {}), hasProSuite: true }
86
+ : { name: 'antd', ...(major !== undefined ? { majorVersion: major } : {}) };
87
+ }
88
+ function detectMui(deps, _projectRoot) {
89
+ if (!('@mui/material' in deps))
90
+ return null;
91
+ const major = majorOf(deps['@mui/material']);
92
+ return { name: 'mui', ...(major !== undefined ? { majorVersion: major } : {}) };
93
+ }
94
+ function detectElementPlus(deps, _projectRoot) {
95
+ return 'element-plus' in deps ? { name: 'element-plus' } : null;
96
+ }
97
+ function detectElementUi(deps, _projectRoot) {
98
+ return 'element-ui' in deps ? { name: 'element-ui' } : null;
99
+ }
100
+ function detectArco(deps, _projectRoot) {
101
+ return '@arco-design/web-react' in deps ? { name: 'arco' } : null;
102
+ }
103
+ function detectTdesign(deps, _projectRoot) {
104
+ return ('tdesign-react' in deps || 'tdesign-vue-next' in deps) ? { name: 'tdesign' } : null;
105
+ }
106
+ function detectSemi(deps, _projectRoot) {
107
+ return '@douyinfe/semi-ui' in deps ? { name: 'semi' } : null;
108
+ }
109
+ function detectNextui(deps, _projectRoot) {
110
+ return '@nextui-org/react' in deps ? { name: 'nextui' } : null;
111
+ }
112
+ function detectChakra(deps, _projectRoot) {
113
+ return '@chakra-ui/react' in deps ? { name: 'chakra' } : null;
114
+ }
115
+ function detectVant(deps, _projectRoot) {
116
+ return 'vant' in deps ? { name: 'vant' } : null;
117
+ }
118
+ /** shadcn / ui is a Tailwind-based component primitive set shipped as
119
+ * copyable source files under `components/ui/`. It has no top-level
120
+ * package of its own, so the signal we use is: at least one of its
121
+ * peer utilities (cva, clsx, tailwind-merge, lucide-react) is present
122
+ * AND Tailwind is present (either as a dep or via a components/ui dir). */
123
+ function detectShadcn(deps, projectRoot) {
124
+ const hasPeerUtility = 'class-variance-authority' in deps
88
125
  || 'clsx' in deps
89
126
  || 'tailwind-merge' in deps
90
- || 'lucide-react' in deps)
91
- && ('tailwindcss' in deps
92
- || existsSync(join(projectRoot, 'components', 'ui'))
93
- || existsSync(join(projectRoot, 'src', 'components', 'ui')))) {
94
- return { name: 'shadcn' };
95
- }
96
- return { name: 'none' };
127
+ || 'lucide-react' in deps;
128
+ if (!hasPeerUtility)
129
+ return null;
130
+ const hasTailwind = 'tailwindcss' in deps
131
+ || existsSync(join(projectRoot, 'components', 'ui'))
132
+ || existsSync(join(projectRoot, 'src', 'components', 'ui'));
133
+ return hasTailwind ? { name: 'shadcn' } : null;
97
134
  }
98
135
  function detectCssFrameworks(projectRoot, deps) {
99
136
  const frameworks = [];
@@ -166,6 +203,11 @@ function detectDataFetching(deps) {
166
203
  }
167
204
  function detectLegacySignals(projectRoot, deps) {
168
205
  const signals = [];
206
+ collectPackageLegacySignals(deps, signals);
207
+ collectSourceFileLegacySignals(projectRoot, signals);
208
+ return signals;
209
+ }
210
+ function collectPackageLegacySignals(deps, signals) {
169
211
  if ('moment' in deps)
170
212
  signals.push('`moment` in deps — prefer `dayjs` or `date-fns` for new code');
171
213
  if (Object.keys(deps).some((d) => d.startsWith('enzyme')))
@@ -180,59 +222,53 @@ function detectLegacySignals(projectRoot, deps) {
180
222
  signals.push('Backbone — legacy; do not add new Backbone code');
181
223
  if (deps['vue']?.startsWith('2') === true)
182
224
  signals.push('Vue 2 — preserve Options API for existing components');
183
- // Lightweight heuristic for class components and inline styles in src/
225
+ }
226
+ function collectSourceFileLegacySignals(projectRoot, signals) {
184
227
  const srcRoot = join(projectRoot, 'src');
185
- if (existsSync(srcRoot)) {
186
- const sample = sampleSourceFiles(srcRoot, 80);
187
- let classComponentHits = 0;
188
- let inlineStyleHits = 0;
189
- for (const filePath of sample) {
190
- try {
191
- const content = readFileSync(filePath, 'utf8');
192
- if (/extends\s+(?:React\.)?Component\b/.test(content))
193
- classComponentHits += 1;
194
- const matches = content.match(/style=\{\{/g);
195
- if (matches !== null)
196
- inlineStyleHits += matches.length;
197
- }
198
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
199
- // ignore unreadable files
200
- }
228
+ if (!existsSync(srcRoot))
229
+ return;
230
+ const sample = sampleSourceFiles(srcRoot, 80);
231
+ const { classComponentHits, inlineStyleHits } = scanSampleForLegacyMarkers(sample);
232
+ if (classComponentHits >= 1)
233
+ signals.push(`React class components detected (${classComponentHits}+ files) — keep class style for existing modules, use function components + hooks for new code`);
234
+ if (inlineStyleHits >= 50)
235
+ signals.push(`Inline styles dominant (${inlineStyleHits}+ occurrences) — match existing styling for new code in same modules`);
236
+ }
237
+ function scanSampleForLegacyMarkers(sample) {
238
+ let classComponentHits = 0;
239
+ let inlineStyleHits = 0;
240
+ for (const filePath of sample) {
241
+ try {
242
+ const content = readFileSync(filePath, 'utf8');
243
+ if (/extends\s+(?:React\.)?Component\b/.test(content))
244
+ classComponentHits += 1;
245
+ const matches = content.match(/style=\{\{/g);
246
+ if (matches !== null)
247
+ inlineStyleHits += matches.length;
248
+ }
249
+ catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
250
+ // ignore unreadable files
201
251
  }
202
- if (classComponentHits >= 1)
203
- signals.push(`React class components detected (${classComponentHits}+ files) — keep class style for existing modules, use function components + hooks for new code`);
204
- if (inlineStyleHits >= 50)
205
- signals.push(`Inline styles dominant (${inlineStyleHits}+ occurrences) — match existing styling for new code in same modules`);
206
252
  }
207
- return signals;
253
+ return { classComponentHits, inlineStyleHits };
208
254
  }
209
255
  function sampleSourceFiles(root, limit) {
210
256
  const out = [];
211
257
  const queue = [root];
212
258
  while (queue.length > 0 && out.length < limit) {
213
259
  const dir = queue.shift();
214
- let entries;
215
- try {
216
- entries = readdirSync(dir);
217
- }
218
- catch {
219
- continue;
220
- }
260
+ const entries = readDirOrEmpty(dir);
221
261
  for (const entry of entries) {
222
- if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist' || entry === 'build')
262
+ if (isIgnoredEntry(entry))
223
263
  continue;
224
264
  const full = join(dir, entry);
225
- let s;
226
- try {
227
- s = statSync(full);
228
- }
229
- catch {
265
+ const s = statOrNull(full);
266
+ if (s === null || s === undefined)
230
267
  continue;
231
- }
232
268
  if (s.isDirectory()) {
233
269
  queue.push(full);
234
270
  }
235
- else if (/\.(tsx|jsx)$/.test(entry)) {
271
+ else if (isJsxFile(entry)) {
236
272
  out.push(full);
237
273
  if (out.length >= limit)
238
274
  break;
@@ -241,6 +277,32 @@ function sampleSourceFiles(root, limit) {
241
277
  }
242
278
  return out;
243
279
  }
280
+ function readDirOrEmpty(dir) {
281
+ try {
282
+ return readdirSync(dir);
283
+ }
284
+ catch {
285
+ return [];
286
+ }
287
+ }
288
+ function statOrNull(path) {
289
+ try {
290
+ return statSync(path);
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ function isIgnoredEntry(entry) {
297
+ if (entry.startsWith('.'))
298
+ return true;
299
+ if (entry === 'node_modules' || entry === 'dist' || entry === 'build')
300
+ return true;
301
+ return false;
302
+ }
303
+ function isJsxFile(entry) {
304
+ return /\.(tsx|jsx)$/.test(entry);
305
+ }
244
306
  function notableDepsList(deps) {
245
307
  const interesting = ['monaco-editor', '@monaco-editor/react', 'react-querybuilder', '@dnd-kit/core', 'react-dnd', 'echarts', 'recharts', '@ant-design/charts', 'antd-style', 'lodash', 'lodash-es', 'rxjs', 'socket.io-client'];
246
308
  return interesting.filter((d) => d in deps);
@@ -23,8 +23,7 @@
23
23
  * "missing → null" contract). Callers can detect "envelope missing"
24
24
  * without throwing.
25
25
  */
26
- import { type SecurityAuditEnvelope } from '../audit-independent/index.js';
27
- import { type PerfAuditEnvelope } from '../audit-independent/index.js';
26
+ import { type PerfAuditEnvelope, type SecurityAuditEnvelope } from '../audit-independent/index.js';
28
27
  import { type KarpathyEnvelope, type MutEnvelope, type QaEnvelope, type AggregatorInput } from './verdict-aggregator.js';
29
28
  /**
30
29
  * `kind` is the discriminator. The on-disk file format is unchanged;
@@ -23,8 +23,7 @@
23
23
  * "missing → null" contract). Callers can detect "envelope missing"
24
24
  * without throwing.
25
25
  */
26
- import { isSecurityAuditEnvelope } from '../audit-independent/index.js';
27
- import { isPerfAuditEnvelope } from '../audit-independent/index.js';
26
+ import { isPerfAuditEnvelope, isSecurityAuditEnvelope } from '../audit-independent/index.js';
28
27
  // ─── 5 parsers (pure, never throw) ─────────────────────────────────────
29
28
  /**
30
29
  * v2.13.3 AC-1 — parse real v2.12.0 audit markdown.
@@ -27,8 +27,7 @@
27
27
  * SecurityAuditEnvelope / PerfAuditEnvelope type re-exports.
28
28
  * - hard precedence; no scoring, no weighting, no RFC voting.
29
29
  */
30
- import type { SecurityAuditEnvelope } from '../audit-independent/index.js';
31
- import type { PerfAuditEnvelope } from '../audit-independent/index.js';
30
+ import type { PerfAuditEnvelope, SecurityAuditEnvelope } from '../audit-independent/index.js';
32
31
  import type { ThresholdViolation } from 'peaks-loop-mut';
33
32
  export interface KarpathyViolation {
34
33
  readonly guideline: 'think-before-coding' | 'simplicity-first' | 'surgical-changes' | 'goal-driven-execution';
@@ -24,13 +24,24 @@ import { normalizePath } from '../../shared/path-utils.js';
24
24
  export const VM_HYPERVISORS = Object.freeze([
25
25
  'kvm', 'hyperkit', 'hyperv'
26
26
  ]);
27
+ // PRD-002b slice 2 — extract lease-TTL primitives (same shape as
28
+ // container-lease.ts / worktree-lease.ts). See config diff comment.
29
+ const MINUTES_PER_HOUR = 60;
30
+ const MS_PER_MINUTE = MINUTES_PER_HOUR * 1_000;
31
+ const LEASE_RD_TTL_MINUTES = 30;
32
+ const LEASE_QA_TTL_MINUTES = 15;
33
+ const LEASE_UI_TTL_MINUTES = MINUTES_PER_HOUR;
34
+ const LEASE_SC_TTL_MINUTES = 30;
35
+ const LEASE_PRD_TTL_MINUTES = 15;
36
+ const LEASE_GENERAL_TTL_MINUTES = 30;
37
+ const LEASE_ID_RANDOM_BYTES = 8;
27
38
  export const DEFAULT_TTL_BY_ROLE = Object.freeze({
28
- rd: 30 * 60 * 1_000,
29
- qa: 15 * 60 * 1_000,
30
- ui: 60 * 60 * 1_000,
31
- sc: 30 * 60 * 1_000,
32
- prd: 15 * 60 * 1_000,
33
- general: 30 * 60 * 1_000
39
+ rd: LEASE_RD_TTL_MINUTES * MS_PER_MINUTE,
40
+ qa: LEASE_QA_TTL_MINUTES * MS_PER_MINUTE,
41
+ ui: LEASE_UI_TTL_MINUTES * MS_PER_MINUTE,
42
+ sc: LEASE_SC_TTL_MINUTES * MS_PER_MINUTE,
43
+ prd: LEASE_PRD_TTL_MINUTES * MS_PER_MINUTE,
44
+ general: LEASE_GENERAL_TTL_MINUTES * MS_PER_MINUTE
34
45
  });
35
46
  export const DEFAULT_TTL_MS = DEFAULT_TTL_BY_ROLE.rd;
36
47
  export const DEFAULT_VM_IMAGE = 'peaks-base:22-slim';
@@ -41,12 +52,12 @@ export function vmLeaseFilePath(sessionRuntimeDir, leaseId) {
41
52
  return joinPath(vmLeaseStoreDir(sessionRuntimeDir), `${leaseId}.json`);
42
53
  }
43
54
  export function generateVmLeaseId() {
44
- return randomBytes(8).toString('hex');
55
+ return randomBytes(LEASE_ID_RANDOM_BYTES).toString('hex');
45
56
  }
46
57
  export function ttlForVmRole(role) {
47
58
  const normalized = role.toLowerCase();
48
59
  const candidate = DEFAULT_TTL_BY_ROLE[normalized];
49
- const fallback = DEFAULT_TTL_BY_ROLE['rd'] ?? 30 * 60 * 1_000;
60
+ const fallback = DEFAULT_TTL_BY_ROLE['rd'] ?? LEASE_RD_TTL_MINUTES * MS_PER_MINUTE;
50
61
  return candidate ?? fallback;
51
62
  }
52
63
  export function finalizeVmLease(draft) {
@@ -24,16 +24,6 @@
24
24
  * implicitly authorizes worktree operations on its path (the hook
25
25
  * integration is Part 2 of this slice).
26
26
  */
27
- /**
28
- * Per-role default TTL. Sub-agent dispatch duration varies by role:
29
- * - rd (long planning + impl) → 30 min
30
- * - qa (test design + browser validation) → 15 min
31
- * - ui (high-fidelity prototype iteration) → 1 h
32
- * - txt (handoff capsule, no worktree expected) → n/a (no worktree)
33
- * - general-purpose / unknown → 30 min (rd-like)
34
- *
35
- * Users MAY override with `--ttl <duration>` (e.g. `--ttl 4h`).
36
- */
37
27
  export declare const DEFAULT_TTL_BY_ROLE: Readonly<Record<string, number>>;
38
28
  export declare const DEFAULT_TTL_MS: number | undefined;
39
29
  export type WorktreeLeaseStatus = 'active' | 'released' | 'expired' | 'gc';
@@ -37,13 +37,24 @@ import { normalizePath } from '../../shared/path-utils.js';
37
37
  *
38
38
  * Users MAY override with `--ttl <duration>` (e.g. `--ttl 4h`).
39
39
  */
40
+ // PRD-002b slice 2 — extract lease-TTL primitives (same shape as
41
+ // container-lease.ts / vm-lease.ts). See config diff comment.
42
+ const MINUTES_PER_HOUR = 60;
43
+ const MS_PER_MINUTE = MINUTES_PER_HOUR * 1_000;
44
+ const LEASE_RD_TTL_MINUTES = 30;
45
+ const LEASE_QA_TTL_MINUTES = 15;
46
+ const LEASE_UI_TTL_MINUTES = MINUTES_PER_HOUR;
47
+ const LEASE_SC_TTL_MINUTES = 30;
48
+ const LEASE_PRD_TTL_MINUTES = 15;
49
+ const LEASE_GENERAL_TTL_MINUTES = 30;
50
+ const LEASE_ID_RANDOM_BYTES = 8;
40
51
  export const DEFAULT_TTL_BY_ROLE = Object.freeze({
41
- rd: 30 * 60 * 1_000,
42
- qa: 15 * 60 * 1_000,
43
- ui: 60 * 60 * 1_000,
44
- sc: 30 * 60 * 1_000,
45
- prd: 15 * 60 * 1_000,
46
- general: 30 * 60 * 1_000
52
+ rd: LEASE_RD_TTL_MINUTES * MS_PER_MINUTE,
53
+ qa: LEASE_QA_TTL_MINUTES * MS_PER_MINUTE,
54
+ ui: LEASE_UI_TTL_MINUTES * MS_PER_MINUTE,
55
+ sc: LEASE_SC_TTL_MINUTES * MS_PER_MINUTE,
56
+ prd: LEASE_PRD_TTL_MINUTES * MS_PER_MINUTE,
57
+ general: LEASE_GENERAL_TTL_MINUTES * MS_PER_MINUTE
47
58
  });
48
59
  export const DEFAULT_TTL_MS = DEFAULT_TTL_BY_ROLE.rd;
49
60
  /**
@@ -76,7 +87,7 @@ export function worktreePath(sessionRuntimeDir, leaseId) {
76
87
  * are irrelevant because each session has its own lease directory.
77
88
  */
78
89
  export function generateLeaseId() {
79
- return randomBytes(8).toString('hex');
90
+ return randomBytes(LEASE_ID_RANDOM_BYTES).toString('hex');
80
91
  }
81
92
  /**
82
93
  * Compute the TTL ms for a role, falling back to DEFAULT_TTL_MS for
@@ -86,7 +97,7 @@ export function generateLeaseId() {
86
97
  export function ttlForRole(role) {
87
98
  const normalized = role.toLowerCase();
88
99
  const candidate = DEFAULT_TTL_BY_ROLE[normalized];
89
- const fallback = DEFAULT_TTL_BY_ROLE['rd'] ?? 30 * 60 * 1_000;
100
+ const fallback = DEFAULT_TTL_BY_ROLE['rd'] ?? LEASE_RD_TTL_MINUTES * MS_PER_MINUTE;
90
101
  return candidate ?? fallback;
91
102
  }
92
103
  /**
@@ -1,4 +1,4 @@
1
- import type { Platform } from './platform.js';
1
+ import { type Platform } from './platform.js';
2
2
  export declare function getDirectoryLinkType(targetPlatform?: Platform): 'junction' | 'dir';
3
3
  export declare function createDirectoryLinkSync(target: string, linkPath: string): void;
4
4
  export declare function readDirectoryLinkTarget(linkPath: string): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.16",
3
+ "version": "4.0.18",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,9 +101,9 @@
101
101
  "headroom-ai": "0.22.4",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^3.25.76",
104
- "peaks-loop-mut": "0.1.19",
105
- "peaks-loop-shared-channel": "0.0.23",
106
- "peaks-loop-shared": "0.0.47"
104
+ "peaks-loop-mut": "0.1.21",
105
+ "peaks-loop-shared": "0.0.49",
106
+ "peaks-loop-shared-channel": "0.0.25"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@changesets/cli": "2.31.1",
@@ -113,9 +113,12 @@
113
113
  "@stryker-mutator/vitest-runner": "^9.6.1",
114
114
  "@types/better-sqlite3": "^7.6.13",
115
115
  "@types/node": "^22.10.2",
116
+ "@typescript-eslint/eslint-plugin": "8.66.0",
117
+ "@typescript-eslint/parser": "8.66.0",
116
118
  "@vitest/coverage-istanbul": "^4.1.10",
117
119
  "@vitest/coverage-v8": "^4.1.10",
118
120
  "c8": "^10.1.3",
121
+ "eslint": "8.57.1",
119
122
  "tsx": "^4.19.2",
120
123
  "typescript": "^5.7.2",
121
124
  "vite": "^6.0.0",
@@ -44,6 +44,52 @@ This skill is the **primary surface**. The `peaks <cmd>` CLI is **auxiliary**
44
44
 
45
45
  **Peaks-Loop Code is an orchestrator, NOT an implementer. You MUST NOT write, edit, or modify any application source code directly.** Every code change goes through `peaks-code → RD → QA → verdict`. **If you catch yourself about to write code, STOP.** Hand off to RD. Before declaring workflow complete, run `peaks workflow verify-pipeline --rid <rid> --project <repo> --json`.
46
46
 
47
+ ### Hard-blocked path families (the orchestrator may NOT directly Edit/Write these)
48
+
49
+ | Family | Examples |
50
+ |---|---|
51
+ | `src/` | `src/services/foo.ts`, `src/cli/commands/x.ts` |
52
+ | `tests/unit/` | `tests/unit/services/foo.test.ts` |
53
+ | `tests/integration/` | `tests/integration/x.test.ts` |
54
+ | `config/` | `config/peaks.json`, `tsconfig.json` |
55
+ | `bin/` | `bin/peaks.js` |
56
+ | `scripts/` | `scripts/release.sh` |
57
+
58
+ ANY direct `Edit` / `Write` / `MultiEdit` of these paths from the orchestrator context is a **hard error**. The orchestrator's role is ONLY to dispatch sub-agents; it never owns a code-change tool call against these families.
59
+
60
+ ### Allowed path families (orchestrator may touch freely)
61
+
62
+ - `.peaks/**` (memory sediment, project metadata, audit logs)
63
+ - `.peaks/_runtime/**` (session-bound reviewable artifacts, gitignored)
64
+ - `skills/**/SKILL.md` and other skill manifest files (orchestrator self-documents)
65
+ - `docs/**` (governance / spec / plan docs)
66
+
67
+ These families are non-source-code; the orchestrator may `Edit`/`Write` them directly (memory sediment, SKILL.md prose updates, doc snapshots). The PreToolUse hook + the `orchestrator-can-do` probe do NOT block these.
68
+
69
+ ### Code-Gate (slice 2026-08-06-codegate-vendor-neutral)
70
+
71
+ Two-layer enforcement — do NOT try to bypass either:
72
+
73
+ 1. **LLM-level guard — `peaks code orchestrator-can-do`** (Step 0.51). Before any Edit/Write/MultiEdit tool call, run:
74
+ ```bash
75
+ peaks code orchestrator-can-do --slice-spec '<your task>' --json
76
+ ```
77
+ For source-code changes (mentioning `src/`, `tests/unit/`, `tests/integration/`, `config/`, `bin/`, `scripts/`) the probe MUST return `canDoInSession: false` with `blockers: ["requires-sub-agent-dispatch"]`. The probe does NOT block non-source slices (memory sediment, docs, SKILL.md updates).
78
+
79
+ 2. **Tool-level guard — `pre-tool-code-gate.sh`** (PreToolUse hook). When the orchestrator reaches for `Edit`/`Write`/`MultiEdit` against a hard-blocked path family, the hook exits 2 with stderr `PEAKS_CODE_PROHIBITED_DIRECT_EDIT: <file>... use 'peaks sub-agent dispatch rd'`. The hook is vendor-neutral — it inspects only the standard `{tool, input}` JSON contract, never env vars.
80
+
81
+ ### If you catch yourself reaching for Edit/Write on `src/**`
82
+
83
+ STOP. Run:
84
+
85
+ ```bash
86
+ peaks sub-agent dispatch rd --prompt '<your task>' --request-id <rid> --project . --batch-id <uuid> --json
87
+ ```
88
+
89
+ The RD sub-agent owns the actual `Edit`/`Write`/`MultiEdit` tool calls against source code. The orchestrator only emits the dispatch request.
90
+
91
+ **Anti-pattern:** directly calling `Edit`/`Write`/`MultiEdit` on `src/**` from the orchestrator session because "the change is tiny" / "it's just one line" / "the dispatch overhead is too high" / "the LLM feels confident". None of these override the Code-Gate; the probe + the hook both fail-closed.
92
+
47
93
  ## 24h mode (orchestrator flag, NOT a new skill)
48
94
 
49
95
  24h mode is a *flag* the orchestrator flips, not a sibling skill. See `.peaks/memory/2026-07-28-peaks-code-loop-skill-proposal.md` for the RL-8 tension analysis. State machine via `peaks session 24h-mode {state,transition,attempts,reset}`; snapshot at `.peaks/_runtime/<sessionId>/24h-state.json` (read-only for peaks-code).