peaks-loop 4.0.48 → 4.0.50
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 +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version stamp for the artifacts `peaks workspace init` generates into a
|
|
3
|
+
* consumer project (G4, 2026-09-15).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `initWorkspace` is the only writer of a consumer project's
|
|
6
|
+
* generated config: `.claude/settings.local.json`, the offline
|
|
7
|
+
* `.peaks/.claude-settings-template.json` copy, and the managed `.gitignore`
|
|
8
|
+
* snippet. `ensureSession` early-returns as soon as a session is bound —
|
|
9
|
+
* deliberately, see its own comment — so after the FIRST init nothing ever
|
|
10
|
+
* re-runs the generator. `npm i -g peaks-loop@<newer>` therefore upgrades the
|
|
11
|
+
* CLI and leaves the project's generated config exactly as the OLD release
|
|
12
|
+
* wrote it.
|
|
13
|
+
*
|
|
14
|
+
* `.claude/settings.local.json` IS drift-checked against the current template
|
|
15
|
+
* — but only when something calls `initWorkspace`, which is precisely the
|
|
16
|
+
* event that stopped happening. The escape hatch (`peaks upgrade
|
|
17
|
+
* --apply-init`) is real and idempotent. What was missing is anything that
|
|
18
|
+
* tells the user — or the LLM driving them — that they need it. That is the
|
|
19
|
+
* gap this module closes: a stamp the generator writes, and a detector that
|
|
20
|
+
* can answer "the file on disk was produced by 4.0.40 while the installed
|
|
21
|
+
* peaks-loop is 4.0.49".
|
|
22
|
+
*
|
|
23
|
+
* WHY A SEPARATE FILE AND NOT A KEY INSIDE THE ARTIFACTS.
|
|
24
|
+
* `.claude/settings.local.json` is read by Claude Code, which owns its schema;
|
|
25
|
+
* a version key peaks invented has no contract there and could be rejected or
|
|
26
|
+
* silently ignored — a stamp nobody reads is worse than none. `.peaks/_runtime/`
|
|
27
|
+
* is peaks' own gitignored tree and already holds exactly this kind of
|
|
28
|
+
* machine-local bookkeeping (`session.json`, `.outer-session-cache.json`), so
|
|
29
|
+
* the stamp lives beside them and needs no new `.gitignore` line.
|
|
30
|
+
*
|
|
31
|
+
* WHAT IS COMPARED. Two versions, because they move independently and a
|
|
32
|
+
* mismatch of either means the same thing to the user (regenerate):
|
|
33
|
+
* - `packageVersion` — the installed peaks-loop release. Moves on `npm i -g`.
|
|
34
|
+
* - `templateVersion` — `TEMPLATE_VERSION`, the shape of the hooks tree this
|
|
35
|
+
* release emits. Can move without a release the user would notice.
|
|
36
|
+
*
|
|
37
|
+
* The stamp records what the generator LAST WROTE. It is not a promise that
|
|
38
|
+
* the on-disk artifacts still match it — a user may have hand-edited
|
|
39
|
+
* `.claude/settings.local.json` since. That question is the existing
|
|
40
|
+
* `templateContentMatches` comparator's, and it is asked on the next init,
|
|
41
|
+
* which is what this detector tells the user to trigger.
|
|
42
|
+
*/
|
|
43
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
44
|
+
import { dirname, join } from 'node:path';
|
|
45
|
+
import { CLI_VERSION } from 'peaks-loop-shared/version';
|
|
46
|
+
import { isExpectedFsMiss } from '../../shared/fs-utils.js';
|
|
47
|
+
import { TEMPLATE_VERSION } from './claude-settings-template.js';
|
|
48
|
+
/**
|
|
49
|
+
* Where the stamp lives, relative to the project root.
|
|
50
|
+
*
|
|
51
|
+
* `.peaks/_runtime/` is gitignored by every peaks-loop project already (see
|
|
52
|
+
* the root `.gitignore` snippet), so this file never shows up in
|
|
53
|
+
* `git status` — which matters, because the whole point is that its content
|
|
54
|
+
* changes on the user's machine and not in their repository.
|
|
55
|
+
*/
|
|
56
|
+
export const GENERATED_ARTIFACTS_STAMP_RELATIVE_PATH = join('.peaks', '_runtime', 'generated-artifacts.json');
|
|
57
|
+
/**
|
|
58
|
+
* The artifacts whose generation the stamp describes. Used to answer "is
|
|
59
|
+
* there anything on disk that could be stale?" — a project that has never run
|
|
60
|
+
* `peaks workspace init` has nothing to refresh and must not be nagged.
|
|
61
|
+
*/
|
|
62
|
+
export const GENERATED_ARTIFACT_RELATIVE_PATHS = [
|
|
63
|
+
join('.claude', 'settings.local.json'),
|
|
64
|
+
join('.peaks', '.claude-settings-template.json')
|
|
65
|
+
];
|
|
66
|
+
export function generatedArtifactsStampPath(projectRoot) {
|
|
67
|
+
return join(projectRoot, GENERATED_ARTIFACTS_STAMP_RELATIVE_PATH);
|
|
68
|
+
}
|
|
69
|
+
/** Does this project have any generated artifact on disk at all? */
|
|
70
|
+
function hasGeneratedArtifact(projectRoot) {
|
|
71
|
+
return GENERATED_ARTIFACT_RELATIVE_PATHS.some((rel) => existsSync(join(projectRoot, rel)));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Read the stamp, or `null` when there is none / it is not the shape this
|
|
75
|
+
* module writes. Tolerant on purpose: a malformed stamp must degrade to
|
|
76
|
+
* "unstamped", never to a crash on the per-turn read path.
|
|
77
|
+
*
|
|
78
|
+
* The catch still NAMES what it tolerates rather than swallowing everything —
|
|
79
|
+
* the same correction the two P1 sites in this slice got (see
|
|
80
|
+
* `isExpectedFsMiss`). A missing file (raced away between `existsSync` and the
|
|
81
|
+
* read) and a stamp we cannot parse both mean "no usable stamp"; a
|
|
82
|
+
* `TypeError` from a broken dependency does not.
|
|
83
|
+
*/
|
|
84
|
+
export function readGeneratedArtifactsStamp(projectRoot) {
|
|
85
|
+
const stampPath = generatedArtifactsStampPath(projectRoot);
|
|
86
|
+
if (!existsSync(stampPath))
|
|
87
|
+
return null;
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(readFileSync(stampPath, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!isExpectedFsMiss(err) && !(err instanceof SyntaxError))
|
|
94
|
+
throw err;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
98
|
+
return null;
|
|
99
|
+
const candidate = parsed;
|
|
100
|
+
if (typeof candidate['packageVersion'] !== 'string' ||
|
|
101
|
+
typeof candidate['templateVersion'] !== 'string' ||
|
|
102
|
+
typeof candidate['writtenAt'] !== 'string') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
stampVersion: 1,
|
|
107
|
+
packageVersion: candidate['packageVersion'],
|
|
108
|
+
templateVersion: candidate['templateVersion'],
|
|
109
|
+
writtenAt: candidate['writtenAt']
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Record what the generator just wrote. Called by `initWorkspace` on every
|
|
114
|
+
* successful materialization — including the one that changes nothing —
|
|
115
|
+
* because the stamp's job is "when did a release last regenerate this
|
|
116
|
+
* project", not "when did the bytes change".
|
|
117
|
+
*
|
|
118
|
+
* `packageVersion` / `now` are injectable so a test can write a deliberately
|
|
119
|
+
* old stamp without touching the clock or the package.
|
|
120
|
+
*/
|
|
121
|
+
export function writeGeneratedArtifactsStamp(projectRoot, options = {}) {
|
|
122
|
+
const stamp = {
|
|
123
|
+
stampVersion: 1,
|
|
124
|
+
packageVersion: options.packageVersion ?? CLI_VERSION,
|
|
125
|
+
templateVersion: TEMPLATE_VERSION,
|
|
126
|
+
writtenAt: (options.now ?? new Date()).toISOString()
|
|
127
|
+
};
|
|
128
|
+
const stampPath = generatedArtifactsStampPath(projectRoot);
|
|
129
|
+
const dir = dirname(stampPath);
|
|
130
|
+
if (!existsSync(dir)) {
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(stampPath, `${JSON.stringify(stamp, null, 2)}\n`, 'utf8');
|
|
134
|
+
return stamp;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Is this project's generated config behind the installed peaks-loop?
|
|
138
|
+
*
|
|
139
|
+
* Returns `stale: false` — with no reasons — for the two cases that are NOT
|
|
140
|
+
* staleness:
|
|
141
|
+
* - the project has no generated artifact at all (never initialized), and
|
|
142
|
+
* - the stamp matches both expected versions.
|
|
143
|
+
*
|
|
144
|
+
* `unstamped` is reported only when an artifact EXISTS and no stamp does:
|
|
145
|
+
* those are projects initialized by a release that predates this module, and
|
|
146
|
+
* they are exactly the population the defect was reported against.
|
|
147
|
+
*/
|
|
148
|
+
export function detectStaleGeneratedArtifacts(projectRoot) {
|
|
149
|
+
const expected = { packageVersion: CLI_VERSION, templateVersion: TEMPLATE_VERSION };
|
|
150
|
+
const onDisk = readGeneratedArtifactsStamp(projectRoot);
|
|
151
|
+
const artifactsPresent = hasGeneratedArtifact(projectRoot);
|
|
152
|
+
if (!artifactsPresent) {
|
|
153
|
+
return { stale: false, reasons: [], onDisk, expected };
|
|
154
|
+
}
|
|
155
|
+
if (existsSync(generatedArtifactsStampPath(projectRoot)) && onDisk === null) {
|
|
156
|
+
return { stale: true, reasons: ['stamp-unreadable'], onDisk, expected };
|
|
157
|
+
}
|
|
158
|
+
if (onDisk === null) {
|
|
159
|
+
return { stale: true, reasons: ['unstamped'], onDisk, expected };
|
|
160
|
+
}
|
|
161
|
+
const reasons = [];
|
|
162
|
+
if (onDisk.packageVersion !== expected.packageVersion)
|
|
163
|
+
reasons.push('package-upgraded');
|
|
164
|
+
if (onDisk.templateVersion !== expected.templateVersion)
|
|
165
|
+
reasons.push('template-changed');
|
|
166
|
+
return { stale: reasons.length > 0, reasons, onDisk, expected };
|
|
167
|
+
}
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* the parent module and calls into this sibling. Function signatures
|
|
10
10
|
* and behaviour are unchanged (verbatim move).
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* The peaks-managed snippet appended to the consumer project's
|
|
14
|
+
* `.peaks/.gitignore` so the local-only settings file never lands
|
|
15
|
+
* in a commit. Marked with a managed-by header so we can detect (and
|
|
16
|
+
* not double-append) on subsequent inits.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PEAKS_GITIGNORE_HEADER = "# >>> peaks-loop managed snippet (slice 2.0.1-bug3) \u2014 do not edit by hand";
|
|
19
|
+
export declare const PEAKS_GITIGNORE_FOOTER = "# <<< peaks-loop managed snippet";
|
|
12
20
|
/**
|
|
13
21
|
* Materialize the consumer-project `.claude/settings.local.json` and
|
|
14
22
|
* ensure the consumer's `.peaks/.gitignore` covers it. Returns a
|
|
@@ -81,6 +81,9 @@ function readEnvObject(serialized) {
|
|
|
81
81
|
* → installAutoCompactHook (installed, 4: … | Bash|Task)
|
|
82
82
|
* → init again (REFRESHED, 3: … ) ← the Bash|Task entry is gone
|
|
83
83
|
*
|
|
84
|
+
* (The 3 is a 2 since TEMPLATE_VERSION 1.8.0 retired the
|
|
85
|
+
* `Write|Edit|MultiEdit` entry. The measurement above is left as taken.)
|
|
86
|
+
*
|
|
84
87
|
* and nothing re-installs it: the hook's whole job was to fire on the next
|
|
85
88
|
* Bash/Task call, so once it is deleted the auto-compact contract stops
|
|
86
89
|
* silently.
|
|
@@ -139,8 +142,13 @@ function isPlainRecord(value) {
|
|
|
139
142
|
* in a commit. Marked with a managed-by header so we can detect (and
|
|
140
143
|
* not double-append) on subsequent inits.
|
|
141
144
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
// Exported (S6, 2026-09-15) so the convergence tests can build a block with
|
|
146
|
+
// the REAL delimiters. A test that retypes them gets a prefix and silently
|
|
147
|
+
// exercises the append path instead of the converge path — which is exactly
|
|
148
|
+
// what happened on the first run of
|
|
149
|
+
// `tests/unit/services/workspace/gitignore-snippet-convergence.test.ts`.
|
|
150
|
+
export const PEAKS_GITIGNORE_HEADER = '# >>> peaks-loop managed snippet (slice 2.0.1-bug3) — do not edit by hand';
|
|
151
|
+
export const PEAKS_GITIGNORE_FOOTER = '# <<< peaks-loop managed snippet';
|
|
144
152
|
const PEAKS_GITIGNORE_SNIPPET = [
|
|
145
153
|
PEAKS_GITIGNORE_HEADER,
|
|
146
154
|
'# Consumer-project .claude/settings.local.json: written by `peaks workspace init`',
|
|
@@ -386,7 +394,34 @@ async function upsertPeaksGitignoreSnippet(projectRoot) {
|
|
|
386
394
|
existing = '';
|
|
387
395
|
}
|
|
388
396
|
}
|
|
389
|
-
|
|
397
|
+
// D2 fix (2026-09-15): the block is now converged, not merely appended.
|
|
398
|
+
//
|
|
399
|
+
// Before this, a project that already had the header returned here forever:
|
|
400
|
+
// the snippet was written once by whichever release first initialized the
|
|
401
|
+
// project and NEVER updated, so a pattern added to the snippet by a later
|
|
402
|
+
// release (`slice 2.0.1-bug3` moved the block from `.peaks/.gitignore` to
|
|
403
|
+
// the root for exactly this class of reason) reached only projects that had
|
|
404
|
+
// not been initialized yet — i.e. new users got the fix and existing users
|
|
405
|
+
// did not, which is backwards.
|
|
406
|
+
//
|
|
407
|
+
// Scope of the rewrite is the managed block, and only the block. Every line
|
|
408
|
+
// outside `HEADER..FOOTER` is spliced through byte-for-byte, which is the
|
|
409
|
+
// "never overwrite the user's edits" semantic this function always had. The
|
|
410
|
+
// header itself says "do not edit by hand"; a user who did is the case
|
|
411
|
+
// `stripLegacyPeaksGitignoreSnippet` already refuses to guess at, and this
|
|
412
|
+
// does the same — a header with no footer is left alone rather than opened
|
|
413
|
+
// up and repaired at the risk of eating the rest of the file.
|
|
414
|
+
const start = existing.indexOf(PEAKS_GITIGNORE_HEADER);
|
|
415
|
+
if (start !== -1) {
|
|
416
|
+
const footerAt = existing.indexOf(PEAKS_GITIGNORE_FOOTER, start + PEAKS_GITIGNORE_HEADER.length);
|
|
417
|
+
if (footerAt === -1)
|
|
418
|
+
return;
|
|
419
|
+
const end = footerAt + PEAKS_GITIGNORE_FOOTER.length;
|
|
420
|
+
const currentBlock = existing.slice(start, end);
|
|
421
|
+
const nextBlock = PEAKS_GITIGNORE_SNIPPET.trimEnd();
|
|
422
|
+
if (currentBlock === nextBlock)
|
|
423
|
+
return;
|
|
424
|
+
await writeFile(gitignorePath, existing.slice(0, start) + nextBlock + existing.slice(end), 'utf8');
|
|
390
425
|
return;
|
|
391
426
|
}
|
|
392
427
|
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
@@ -363,6 +363,15 @@ export async function initWorkspace(options) {
|
|
|
363
363
|
.map((write) => write.relativePath)
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
|
+
const claudeSettings = await materializeClaudeSettingsLocal(options.projectRoot, options.noClaudeHooks === true);
|
|
367
|
+
// G4 (2026-09-15): record which release generated this project's config.
|
|
368
|
+
// Without it there is nothing on disk for a LATER release to compare
|
|
369
|
+
// against, so "your generated config is behind the installed peaks-loop"
|
|
370
|
+
// is not a decidable question — which is the whole of D1. Written on every
|
|
371
|
+
// init (including the no-op ones): the stamp answers "when did a release
|
|
372
|
+
// last regenerate this project", not "when did the bytes change". See
|
|
373
|
+
// `generated-artifacts-stamp.ts`.
|
|
374
|
+
writeGeneratedArtifactsStamp(options.projectRoot);
|
|
366
375
|
return {
|
|
367
376
|
sessionId: options.sessionId,
|
|
368
377
|
sessionRoot,
|
|
@@ -370,7 +379,7 @@ export async function initWorkspace(options) {
|
|
|
370
379
|
alreadyExisted,
|
|
371
380
|
bound,
|
|
372
381
|
previousSessionId,
|
|
373
|
-
claudeSettings
|
|
382
|
+
claudeSettings,
|
|
374
383
|
standardsMissing,
|
|
375
384
|
...(standardsApplied !== undefined ? { standardsApplied } : {})
|
|
376
385
|
};
|
|
@@ -385,6 +394,7 @@ export async function initWorkspace(options) {
|
|
|
385
394
|
// are unchanged (verbatim move).
|
|
386
395
|
import { materializeClaudeSettingsLocal } from './workspace-claude-settings-materializer.js';
|
|
387
396
|
export { materializeClaudeSettingsLocal } from './workspace-claude-settings-materializer.js';
|
|
397
|
+
import { writeGeneratedArtifactsStamp } from './generated-artifacts-stamp.js';
|
|
388
398
|
/**
|
|
389
399
|
* Slice C10 (2026-06-24-legacy-change-id-sibling): whole-dir shape check
|
|
390
400
|
* for the legacy sibling `.peaks/_runtime/<sessionId>/`. Returns `true` ONLY when
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { type Platform } from './platform.js';
|
|
2
|
+
/**
|
|
3
|
+
* Does this error mean "the path is legitimately absent or unreadable by us"?
|
|
4
|
+
*
|
|
5
|
+
* A `catch` is allowed to turn THAT class of failure into a fallback value.
|
|
6
|
+
* Every other error is a bug and must propagate. Without the distinction,
|
|
7
|
+
* "this catch only swallows IO errors" is a claim the code cannot keep:
|
|
8
|
+
*
|
|
9
|
+
* catch (err) {
|
|
10
|
+
* if (err instanceof ReferenceError) throw err;
|
|
11
|
+
* if (err instanceof SyntaxError) throw err;
|
|
12
|
+
* return null; // ← swallows every TypeError, every ERR_INVALID_ARG_TYPE,
|
|
13
|
+
* // every error a dependency throws, and calls it "IO"
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* That exact shape sat on two priority sites (`post-compact-detector`,
|
|
17
|
+
* `step-08-gate`) with that exact comment. Rethrowing the two JS error
|
|
18
|
+
* classes it happened to name is not the same rule as swallowing one error
|
|
19
|
+
* class and rethrowing the rest — and it is the *inverted* one: the more
|
|
20
|
+
* unexpected the failure, the more certainly it was swallowed.
|
|
21
|
+
*
|
|
22
|
+
* The set is deliberately the "miss" codes only. `EISDIR` is included because
|
|
23
|
+
* reading a directory where a file was expected is indistinguishable from
|
|
24
|
+
* absent to every caller here, and excluding it would turn a pre-existing
|
|
25
|
+
* silent fallback into a new loud failure for no gain.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isExpectedFsMiss(err: unknown): boolean;
|
|
2
28
|
export declare function getDirectoryLinkType(targetPlatform?: Platform): 'junction' | 'dir';
|
|
3
29
|
export declare function createDirectoryLinkSync(target: string, linkPath: string): void;
|
|
4
30
|
export declare function readDirectoryLinkTarget(linkPath: string): string | null;
|
package/dist/shared/fs-utils.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { symlinkSync as nodeSymlinkSync, readlinkSync } from 'node:fs';
|
|
2
2
|
import { platform } from './platform.js';
|
|
3
|
+
/**
|
|
4
|
+
* Does this error mean "the path is legitimately absent or unreadable by us"?
|
|
5
|
+
*
|
|
6
|
+
* A `catch` is allowed to turn THAT class of failure into a fallback value.
|
|
7
|
+
* Every other error is a bug and must propagate. Without the distinction,
|
|
8
|
+
* "this catch only swallows IO errors" is a claim the code cannot keep:
|
|
9
|
+
*
|
|
10
|
+
* catch (err) {
|
|
11
|
+
* if (err instanceof ReferenceError) throw err;
|
|
12
|
+
* if (err instanceof SyntaxError) throw err;
|
|
13
|
+
* return null; // ← swallows every TypeError, every ERR_INVALID_ARG_TYPE,
|
|
14
|
+
* // every error a dependency throws, and calls it "IO"
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* That exact shape sat on two priority sites (`post-compact-detector`,
|
|
18
|
+
* `step-08-gate`) with that exact comment. Rethrowing the two JS error
|
|
19
|
+
* classes it happened to name is not the same rule as swallowing one error
|
|
20
|
+
* class and rethrowing the rest — and it is the *inverted* one: the more
|
|
21
|
+
* unexpected the failure, the more certainly it was swallowed.
|
|
22
|
+
*
|
|
23
|
+
* The set is deliberately the "miss" codes only. `EISDIR` is included because
|
|
24
|
+
* reading a directory where a file was expected is indistinguishable from
|
|
25
|
+
* absent to every caller here, and excluding it would turn a pre-existing
|
|
26
|
+
* silent fallback into a new loud failure for no gain.
|
|
27
|
+
*/
|
|
28
|
+
export function isExpectedFsMiss(err) {
|
|
29
|
+
const code = err?.code;
|
|
30
|
+
return (code === 'ENOENT' ||
|
|
31
|
+
code === 'ENOTDIR' ||
|
|
32
|
+
code === 'EISDIR' ||
|
|
33
|
+
code === 'EACCES' ||
|
|
34
|
+
code === 'EPERM' ||
|
|
35
|
+
code === 'ELOOP' ||
|
|
36
|
+
code === 'ENAMETOOLONG');
|
|
37
|
+
}
|
|
3
38
|
export function getDirectoryLinkType(targetPlatform = platform) {
|
|
4
39
|
return targetPlatform === 'win32' ? 'junction' : 'dir';
|
|
5
40
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one seam through which a `.peaks/_runtime` path is built.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-09-15 (runtime-path-unrepresentable). Three attempts to *detect*
|
|
5
|
+
* a caller-supplied id reaching a runtime join all failed the same way: the
|
|
6
|
+
* shipped text rule caught 4 of 12 fixture shapes where the name-based
|
|
7
|
+
* predicate it replaced caught 8, and the version that reached 10 of 12 gave
|
|
8
|
+
* back the change that reached 12 because it cost seven findings on live code —
|
|
9
|
+
* two of them structural (a guard helper, and readers that take the guarded
|
|
10
|
+
* value as a parameter). Its measured verdict: reassignment between guard and
|
|
11
|
+
* join, and guard-after-join, are **domination failures inside a single
|
|
12
|
+
* function, invisible to any text key**.
|
|
13
|
+
*
|
|
14
|
+
* So the instrument is retired in favour of a property. The id cannot reach a
|
|
15
|
+
* runtime join unguarded because there is no longer a join that accepts an
|
|
16
|
+
* unguarded string: `RuntimeRoot.join` takes `GuardedSegment`, and a
|
|
17
|
+
* `GuardedSegment` can only be produced by `guardRuntimeSegment`, which throws
|
|
18
|
+
* on the shapes the escapes used. A newly written unguarded join is a type
|
|
19
|
+
* error at authoring time — not a removed guard that some later scan notices.
|
|
20
|
+
*
|
|
21
|
+
* The raw root is not obtainable as a string except through `dir()`, which is
|
|
22
|
+
* named so that a join written from it (`join(root.dir(), id)`) reads at review
|
|
23
|
+
* time as the bypass it is. `dir()` exists because callers legitimately
|
|
24
|
+
* enumerate the root itself; it is not a join.
|
|
25
|
+
*/
|
|
26
|
+
declare const RUNTIME_SEGMENT: unique symbol;
|
|
27
|
+
/**
|
|
28
|
+
* A path segment that has been checked as a single safe segment.
|
|
29
|
+
*
|
|
30
|
+
* Unforgeable by construction: the brand is a `unique symbol` that is never
|
|
31
|
+
* exported, so `value as GuardedSegment` outside this module is a type error
|
|
32
|
+
* and a plain `string` is not assignable. `guardRuntimeSegment` is the only
|
|
33
|
+
* producer.
|
|
34
|
+
*/
|
|
35
|
+
export type GuardedSegment = string & {
|
|
36
|
+
readonly [RUNTIME_SEGMENT]: true;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Check a caller-supplied string and brand it for use as a runtime path
|
|
40
|
+
* segment. `label` names the id in the error the way the caller knows it
|
|
41
|
+
* ("session id", "project id"), because the throw site is one function away
|
|
42
|
+
* from the caller that supplied it.
|
|
43
|
+
*
|
|
44
|
+
* Rejects exactly the shapes `isUnsafePathInput` rejects: separators, `..`,
|
|
45
|
+
* absolute and drive-prefixed paths, UNC and URL shapes, and empty segments.
|
|
46
|
+
*/
|
|
47
|
+
export declare function guardRuntimeSegment(value: string, label: string): GuardedSegment;
|
|
48
|
+
/**
|
|
49
|
+
* The `.peaks/_runtime` root of one project, as a capability rather than a
|
|
50
|
+
* string. `#path` is a private field, so the raw root cannot be read off the
|
|
51
|
+
* object and joined by an unguarded `join()`.
|
|
52
|
+
*/
|
|
53
|
+
export declare class RuntimeRoot {
|
|
54
|
+
#private;
|
|
55
|
+
private constructor();
|
|
56
|
+
/** The runtime root of `projectRoot`. */
|
|
57
|
+
static at(projectRoot: string): RuntimeRoot;
|
|
58
|
+
/**
|
|
59
|
+
* Join guarded segments onto the root. At least one segment is required: a
|
|
60
|
+
* zero-argument `join()` would hand back the bare root as a `string`, which
|
|
61
|
+
* is the capability this class exists to withhold.
|
|
62
|
+
*/
|
|
63
|
+
join(first: GuardedSegment, ...rest: readonly GuardedSegment[]): string;
|
|
64
|
+
/**
|
|
65
|
+
* The root itself, for READ-only enumeration (`readdir`, `existsSync`) — not
|
|
66
|
+
* for joining. Deliberately a method rather than a property so a bypass is
|
|
67
|
+
* legible at the call site.
|
|
68
|
+
*/
|
|
69
|
+
dir(): string;
|
|
70
|
+
}
|
|
71
|
+
/** The runtime root of `projectRoot`. */
|
|
72
|
+
export declare function runtimeRoot(projectRoot: string): RuntimeRoot;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one seam through which a `.peaks/_runtime` path is built.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-09-15 (runtime-path-unrepresentable). Three attempts to *detect*
|
|
5
|
+
* a caller-supplied id reaching a runtime join all failed the same way: the
|
|
6
|
+
* shipped text rule caught 4 of 12 fixture shapes where the name-based
|
|
7
|
+
* predicate it replaced caught 8, and the version that reached 10 of 12 gave
|
|
8
|
+
* back the change that reached 12 because it cost seven findings on live code —
|
|
9
|
+
* two of them structural (a guard helper, and readers that take the guarded
|
|
10
|
+
* value as a parameter). Its measured verdict: reassignment between guard and
|
|
11
|
+
* join, and guard-after-join, are **domination failures inside a single
|
|
12
|
+
* function, invisible to any text key**.
|
|
13
|
+
*
|
|
14
|
+
* So the instrument is retired in favour of a property. The id cannot reach a
|
|
15
|
+
* runtime join unguarded because there is no longer a join that accepts an
|
|
16
|
+
* unguarded string: `RuntimeRoot.join` takes `GuardedSegment`, and a
|
|
17
|
+
* `GuardedSegment` can only be produced by `guardRuntimeSegment`, which throws
|
|
18
|
+
* on the shapes the escapes used. A newly written unguarded join is a type
|
|
19
|
+
* error at authoring time — not a removed guard that some later scan notices.
|
|
20
|
+
*
|
|
21
|
+
* The raw root is not obtainable as a string except through `dir()`, which is
|
|
22
|
+
* named so that a join written from it (`join(root.dir(), id)`) reads at review
|
|
23
|
+
* time as the bypass it is. `dir()` exists because callers legitimately
|
|
24
|
+
* enumerate the root itself; it is not a join.
|
|
25
|
+
*/
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { isUnsafePathInput } from './path-safety.js';
|
|
28
|
+
/**
|
|
29
|
+
* Check a caller-supplied string and brand it for use as a runtime path
|
|
30
|
+
* segment. `label` names the id in the error the way the caller knows it
|
|
31
|
+
* ("session id", "project id"), because the throw site is one function away
|
|
32
|
+
* from the caller that supplied it.
|
|
33
|
+
*
|
|
34
|
+
* Rejects exactly the shapes `isUnsafePathInput` rejects: separators, `..`,
|
|
35
|
+
* absolute and drive-prefixed paths, UNC and URL shapes, and empty segments.
|
|
36
|
+
*/
|
|
37
|
+
export function guardRuntimeSegment(value, label) {
|
|
38
|
+
if (isUnsafePathInput(value)) {
|
|
39
|
+
throw new Error(`Invalid ${label}: ${value} (must be a single path segment)`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The `.peaks/_runtime` root of one project, as a capability rather than a
|
|
45
|
+
* string. `#path` is a private field, so the raw root cannot be read off the
|
|
46
|
+
* object and joined by an unguarded `join()`.
|
|
47
|
+
*/
|
|
48
|
+
export class RuntimeRoot {
|
|
49
|
+
#path;
|
|
50
|
+
constructor(path) {
|
|
51
|
+
this.#path = path;
|
|
52
|
+
}
|
|
53
|
+
/** The runtime root of `projectRoot`. */
|
|
54
|
+
static at(projectRoot) {
|
|
55
|
+
return new RuntimeRoot(join(projectRoot, '.peaks', '_runtime'));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Join guarded segments onto the root. At least one segment is required: a
|
|
59
|
+
* zero-argument `join()` would hand back the bare root as a `string`, which
|
|
60
|
+
* is the capability this class exists to withhold.
|
|
61
|
+
*/
|
|
62
|
+
join(first, ...rest) {
|
|
63
|
+
return join(this.#path, first, ...rest);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The root itself, for READ-only enumeration (`readdir`, `existsSync`) — not
|
|
67
|
+
* for joining. Deliberately a method rather than a property so a bypass is
|
|
68
|
+
* legible at the call site.
|
|
69
|
+
*/
|
|
70
|
+
dir() {
|
|
71
|
+
return this.#path;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The runtime root of `projectRoot`. */
|
|
75
|
+
export function runtimeRoot(projectRoot) {
|
|
76
|
+
return RuntimeRoot.at(projectRoot);
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.50",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -102,10 +102,10 @@
|
|
|
102
102
|
"picomatch": "4.0.4",
|
|
103
103
|
"yaml": "^2.9.0",
|
|
104
104
|
"zod": "^4.4.3",
|
|
105
|
-
"peaks-loop-
|
|
106
|
-
"peaks-loop-
|
|
107
|
-
"peaks-loop-shared": "0.0.
|
|
108
|
-
"peaks-loop-
|
|
105
|
+
"peaks-loop-mut": "0.1.48",
|
|
106
|
+
"peaks-loop-internal-runtime": "0.0.35",
|
|
107
|
+
"peaks-loop-shared-channel": "0.0.52",
|
|
108
|
+
"peaks-loop-shared": "0.0.84"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@changesets/cli": "2.31.1",
|
|
@@ -141,6 +141,8 @@
|
|
|
141
141
|
"test:dev": "vitest run tests/unit",
|
|
142
142
|
"test:dev:cli": "vitest run tests/unit",
|
|
143
143
|
"test:unit": "vitest run tests/unit",
|
|
144
|
+
"test:e2e": "vitest run --config vitest.config.e2e.ts",
|
|
145
|
+
"test:lint": "vitest run --config vitest.config.lint.ts",
|
|
144
146
|
"test:integration": "vitest run --config vitest.config.integration.ts tests/integration",
|
|
145
147
|
"test:capability-guard": "vitest run --config vitest.config.integration.ts tests/integration/capability-guard",
|
|
146
148
|
"test:cli": "vitest run tests/unit",
|
|
@@ -149,8 +151,8 @@
|
|
|
149
151
|
"test:ci": "node ./scripts/sync-version.mjs && node ./scripts/lint/silent-warning-detector.mjs && vitest run --coverage",
|
|
150
152
|
"test:changed": "node scripts/test-changed.mjs",
|
|
151
153
|
"lint:silent-warning": "node ./scripts/lint/silent-warning-detector.mjs",
|
|
152
|
-
"test:race": "vitest run --no-file-parallelism packages/peaks-loop-shared-channel/tests/shared-channel.test.ts tests/unit/dispatch-record-writer.test.ts
|
|
153
|
-
"test:replay": "node scripts/fixture-capture-setup.mjs && vitest run tests/unit/replay tests/unit/
|
|
154
|
+
"test:race": "vitest run --no-file-parallelism packages/peaks-loop-shared-channel/tests/shared-channel.test.ts tests/unit/services/dispatch/dispatch-record-writer.test.ts",
|
|
155
|
+
"test:replay": "node scripts/fixture-capture-setup.mjs && vitest run tests/unit/services/dispatch/conflict-replay.test.ts tests/unit/services/dispatch/e2e-fixtures.test.ts",
|
|
154
156
|
"fixture:capture-setup": "node scripts/fixture-capture-setup.mjs",
|
|
155
157
|
"test:coverage": "node ./scripts/coverage-c8.mjs",
|
|
156
158
|
"test:coverage:c8": "node ./scripts/coverage-c8.mjs",
|
|
@@ -60,18 +60,6 @@ const targets = [
|
|
|
60
60
|
src: join(packageRoot, 'src/services/skillhub/migrations'),
|
|
61
61
|
dest: join(packageRoot, 'dist/services/skillhub/migrations'),
|
|
62
62
|
extensions: ['.sql']
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
// Slice c5-write-hook-exec-form: the Write|Edit|MultiEdit PreToolUse gate
|
|
66
|
-
// emitted by `peaks workspace init` is invoked as `node <path>`, and the
|
|
67
|
-
// path is resolved relative to this module — `dist/services/hooks/` in an
|
|
68
|
-
// installed consumer, `src/services/hooks/` under tsx. A plain `.js` asset
|
|
69
|
-
// (not a `.ts` compiled by tsc) is what lets ONE relative filename be valid
|
|
70
|
-
// in both trees. Without this copy the hook would be a broken path in every
|
|
71
|
-
// installed consumer while every test in this repo still passed.
|
|
72
|
-
src: join(packageRoot, 'src/services/hooks'),
|
|
73
|
-
dest: join(packageRoot, 'dist/services/hooks'),
|
|
74
|
-
extensions: ['.js']
|
|
75
63
|
}
|
|
76
64
|
];
|
|
77
65
|
|