peaks-cli 1.1.2 → 1.2.0
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/README.md +97 -3
- package/dist/src/cli/commands/core-artifact-commands.js +47 -3
- package/dist/src/cli/commands/gate-commands.d.ts +3 -0
- package/dist/src/cli/commands/gate-commands.js +103 -0
- package/dist/src/cli/commands/hooks-commands.d.ts +3 -0
- package/dist/src/cli/commands/hooks-commands.js +75 -0
- package/dist/src/cli/commands/request-commands.js +1 -1
- package/dist/src/cli/commands/sop-commands.d.ts +3 -0
- package/dist/src/cli/commands/sop-commands.js +215 -0
- package/dist/src/cli/index.js +12 -0
- package/dist/src/cli/program.js +47 -2
- package/dist/src/services/dashboard/project-dashboard-service.js +5 -3
- package/dist/src/services/mode/mode-enforcement.js +2 -1
- package/dist/src/services/skills/hooks-settings-service.d.ts +45 -0
- package/dist/src/services/skills/hooks-settings-service.js +167 -0
- package/dist/src/services/sop/gate-enforce-service.d.ts +33 -0
- package/dist/src/services/sop/gate-enforce-service.js +168 -0
- package/dist/src/services/sop/sop-advance-service.d.ts +74 -0
- package/dist/src/services/sop/sop-advance-service.js +115 -0
- package/dist/src/services/sop/sop-check-service.d.ts +29 -0
- package/dist/src/services/sop/sop-check-service.js +148 -0
- package/dist/src/services/sop/sop-paths.d.ts +62 -0
- package/dist/src/services/sop/sop-paths.js +92 -0
- package/dist/src/services/sop/sop-registry-service.d.ts +46 -0
- package/dist/src/services/sop/sop-registry-service.js +89 -0
- package/dist/src/services/sop/sop-service.d.ts +47 -0
- package/dist/src/services/sop/sop-service.js +211 -0
- package/dist/src/services/sop/sop-types.d.ts +88 -0
- package/dist/src/services/sop/sop-types.js +11 -0
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +2 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/schemas/sop-manifest.schema.json +52 -0
- package/skills/peaks-sop/SKILL.md +192 -0
- package/skills/peaks-sop/references/sop-authoring.md +161 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { SOP_GATE_CHECK_TYPES, SOP_ID_PATTERN } from './sop-types.js';
|
|
4
|
+
import { sopDir, sopManifestPath, sopSkillPath, projectSopDir, projectSopManifestPath, projectSopSkillPath, resolveSopManifestPath } from './sop-paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* SOP authoring substrate — Feature A, Slice 1.
|
|
7
|
+
*
|
|
8
|
+
* `initSop` scaffolds a user-authored SOP (manifest + registrable SKILL.md);
|
|
9
|
+
* `lintSop` validates the manifest. No registry, no enforcement here — Slice 1
|
|
10
|
+
* only lets users create and validate a SOP. The SOP id grammar
|
|
11
|
+
* (SOP_ID_PATTERN: lowercase kebab, no dots/slashes) doubles as the path
|
|
12
|
+
* traversal guard, matching how request artifacts guard their ids.
|
|
13
|
+
*
|
|
14
|
+
* SOP definitions are GLOBAL (`~/.peaks/sops/<id>/`, see ./sop-paths) so one
|
|
15
|
+
* authored SOP is reusable across projects; only run-state is per-project. The
|
|
16
|
+
* authoring ops here therefore take no projectRoot.
|
|
17
|
+
*/
|
|
18
|
+
const RESERVED_ID_PREFIX = 'peaks-';
|
|
19
|
+
const RESERVED_IDS = new Set(['peaks']);
|
|
20
|
+
/**
|
|
21
|
+
* Read and JSON-parse a SOP manifest for EXECUTION, resolving project-first then
|
|
22
|
+
* global (the project layer wins). Returns null when neither layer has it;
|
|
23
|
+
* throws on malformed JSON. Callers that need validation should run lintSop.
|
|
24
|
+
*/
|
|
25
|
+
export async function readSopManifest(id, projectRoot) {
|
|
26
|
+
const resolved = resolveSopManifestPath(id, projectRoot);
|
|
27
|
+
if (resolved === null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return JSON.parse(await readFile(resolved.path, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
/** Why an id cannot be used; null when the id is acceptable. */
|
|
33
|
+
function reservedIdReason(id) {
|
|
34
|
+
if (id.startsWith(RESERVED_ID_PREFIX) || RESERVED_IDS.has(id)) {
|
|
35
|
+
return `SOP id "${id}" collides with the reserved built-in peaks-* namespace`;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function scaffoldManifest(id, name) {
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
name,
|
|
43
|
+
description: '',
|
|
44
|
+
phases: ['draft', 'review', 'done'],
|
|
45
|
+
gates: [
|
|
46
|
+
{ id: 'example-gate', phase: 'review', check: { type: 'file-exists', path: 'README.md' } }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function scaffoldSkill(manifest) {
|
|
51
|
+
const phaseList = manifest.phases.join(' → ');
|
|
52
|
+
return [
|
|
53
|
+
'---',
|
|
54
|
+
`name: ${manifest.id}`,
|
|
55
|
+
`description: User-authored Peaks SOP "${manifest.name}". Phases: ${phaseList}.`,
|
|
56
|
+
'---',
|
|
57
|
+
'',
|
|
58
|
+
`# ${manifest.name}`,
|
|
59
|
+
'',
|
|
60
|
+
'A user-authored SOP. Edit `sop.json` to define phases and gates, then run',
|
|
61
|
+
'`peaks sop lint` to validate it.',
|
|
62
|
+
'',
|
|
63
|
+
'## Phases',
|
|
64
|
+
'',
|
|
65
|
+
...manifest.phases.map((phase) => `- ${phase}`),
|
|
66
|
+
''
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
export async function initSop(options) {
|
|
70
|
+
if (!SOP_ID_PATTERN.test(options.id)) {
|
|
71
|
+
throw new Error(`Invalid SOP id: ${options.id} (expected lowercase letters, digits, and dashes, starting alphanumeric)`);
|
|
72
|
+
}
|
|
73
|
+
const reserved = reservedIdReason(options.id);
|
|
74
|
+
if (reserved !== null) {
|
|
75
|
+
throw new Error(reserved);
|
|
76
|
+
}
|
|
77
|
+
const inProject = options.projectRoot !== undefined;
|
|
78
|
+
const dir = inProject ? projectSopDir(options.projectRoot, options.id) : sopDir(options.id);
|
|
79
|
+
const manifestPath = inProject ? projectSopManifestPath(options.projectRoot, options.id) : sopManifestPath(options.id);
|
|
80
|
+
const skillPath = inProject ? projectSopSkillPath(options.projectRoot, options.id) : sopSkillPath(options.id);
|
|
81
|
+
if (existsSync(manifestPath)) {
|
|
82
|
+
throw new Error(`A SOP with id "${options.id}" already exists at ${manifestPath}. Remove it before re-running peaks sop init.`);
|
|
83
|
+
}
|
|
84
|
+
const manifest = scaffoldManifest(options.id, options.name ?? options.id);
|
|
85
|
+
const skillContent = scaffoldSkill(manifest);
|
|
86
|
+
const result = {
|
|
87
|
+
id: options.id,
|
|
88
|
+
dir,
|
|
89
|
+
manifestPath,
|
|
90
|
+
skillPath,
|
|
91
|
+
manifest,
|
|
92
|
+
skillContent,
|
|
93
|
+
applied: false
|
|
94
|
+
};
|
|
95
|
+
if (options.apply !== true) {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
await mkdir(dir, { recursive: true });
|
|
99
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
100
|
+
await writeFile(skillPath, skillContent, 'utf8');
|
|
101
|
+
return { ...result, applied: true };
|
|
102
|
+
}
|
|
103
|
+
function pushError(findings, code, message, gateId) {
|
|
104
|
+
findings.push(gateId === undefined ? { code, message, severity: 'error' } : { code, message, gateId, severity: 'error' });
|
|
105
|
+
}
|
|
106
|
+
function lintGate(gate, index, phases, seenGateIds, allowCommands, findings) {
|
|
107
|
+
const label = typeof gate?.id === 'string' && gate.id.length > 0 ? gate.id : `#${index}`;
|
|
108
|
+
if (typeof gate?.id !== 'string' || !SOP_ID_PATTERN.test(gate.id)) {
|
|
109
|
+
pushError(findings, 'INVALID_GATE_ID', `Gate ${label} has an invalid id (expected lowercase kebab)`, label);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (seenGateIds.has(gate.id)) {
|
|
113
|
+
pushError(findings, 'DUPLICATE_GATE_ID', `Duplicate gate id "${gate.id}"`, gate.id);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
seenGateIds.add(gate.id);
|
|
117
|
+
if (typeof gate.phase !== 'string' || !phases.has(gate.phase)) {
|
|
118
|
+
pushError(findings, 'GATE_PHASE_UNKNOWN', `Gate "${gate.id}" binds to unknown phase "${String(gate.phase)}"`, gate.id);
|
|
119
|
+
}
|
|
120
|
+
const check = gate.check;
|
|
121
|
+
if (check === null || typeof check !== 'object' || !SOP_GATE_CHECK_TYPES.includes(check.type ?? '')) {
|
|
122
|
+
pushError(findings, 'INVALID_CHECK_TYPE', `Gate "${gate.id}" has an invalid or missing check type (expected ${SOP_GATE_CHECK_TYPES.join(' | ')})`, gate.id);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (check.type === 'file-exists' && (typeof check.path !== 'string' || check.path.length === 0)) {
|
|
126
|
+
pushError(findings, 'CHECK_MISSING_FIELD', `Gate "${gate.id}" file-exists check requires a non-empty "path"`, gate.id);
|
|
127
|
+
}
|
|
128
|
+
if (check.type === 'grep' && (typeof check.file !== 'string' || check.file.length === 0 || typeof check.pattern !== 'string' || check.pattern.length === 0)) {
|
|
129
|
+
pushError(findings, 'CHECK_MISSING_FIELD', `Gate "${gate.id}" grep check requires non-empty "file" and "pattern"`, gate.id);
|
|
130
|
+
}
|
|
131
|
+
if (check.type === 'command') {
|
|
132
|
+
if (!Array.isArray(check.run) || check.run.length === 0 || !check.run.every((part) => typeof part === 'string')) {
|
|
133
|
+
pushError(findings, 'CHECK_MISSING_FIELD', `Gate "${gate.id}" command check requires a non-empty string array "run"`, gate.id);
|
|
134
|
+
}
|
|
135
|
+
if (!allowCommands) {
|
|
136
|
+
pushError(findings, 'COMMAND_NOT_ALLOWED', `Gate "${gate.id}" uses a command check; re-run with --allow-commands to permit command-type gates`, gate.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function lintGuard(guard, index, phases, findings) {
|
|
141
|
+
const label = `#${index}`;
|
|
142
|
+
if (typeof guard?.phase !== 'string' || !phases.has(guard.phase)) {
|
|
143
|
+
pushError(findings, 'GUARD_PHASE_UNKNOWN', `Guard ${label} binds to unknown phase "${String(guard?.phase)}"`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof guard?.bash !== 'string' || guard.bash.length === 0) {
|
|
146
|
+
pushError(findings, 'GUARD_MISSING_PATTERN', `Guard ${label} requires a non-empty "bash" pattern`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
// eslint-disable-next-line no-new
|
|
151
|
+
new RegExp(guard.bash);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
pushError(findings, 'GUARD_INVALID_PATTERN', `Guard ${label} has an invalid bash regex "${guard.bash}"`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function lintSop(options) {
|
|
158
|
+
// lint validates the EXACT layer the caller targets (project when projectRoot
|
|
159
|
+
// is set, else global) — not the precedence resolution used for execution.
|
|
160
|
+
const manifestPath = options.projectRoot !== undefined
|
|
161
|
+
? projectSopManifestPath(options.projectRoot, options.id)
|
|
162
|
+
: sopManifestPath(options.id);
|
|
163
|
+
if (!existsSync(manifestPath)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const findings = [];
|
|
167
|
+
let manifest = null;
|
|
168
|
+
try {
|
|
169
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
pushError(findings, 'INVALID_JSON', `Manifest is not valid JSON: ${error instanceof Error ? error.message : 'parse error'}`);
|
|
173
|
+
return { ok: false, id: options.id, manifestPath, gateCount: 0, gateIds: [], findings };
|
|
174
|
+
}
|
|
175
|
+
if (typeof manifest.id !== 'string' || !SOP_ID_PATTERN.test(manifest.id)) {
|
|
176
|
+
pushError(findings, 'INVALID_ID', `Manifest id "${String(manifest.id)}" is invalid (expected lowercase kebab)`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const reserved = reservedIdReason(manifest.id);
|
|
180
|
+
if (reserved !== null) {
|
|
181
|
+
pushError(findings, 'RESERVED_ID', reserved);
|
|
182
|
+
}
|
|
183
|
+
if (manifest.id !== options.id) {
|
|
184
|
+
pushError(findings, 'ID_MISMATCH', `Manifest id "${manifest.id}" does not match its directory "${options.id}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const phases = Array.isArray(manifest.phases) ? manifest.phases : [];
|
|
188
|
+
if (phases.length === 0) {
|
|
189
|
+
pushError(findings, 'EMPTY_PHASES', 'Manifest must declare at least one phase');
|
|
190
|
+
}
|
|
191
|
+
const phaseSet = new Set();
|
|
192
|
+
for (const phase of phases) {
|
|
193
|
+
if (phaseSet.has(phase)) {
|
|
194
|
+
pushError(findings, 'DUPLICATE_PHASE', `Duplicate phase "${phase}"`);
|
|
195
|
+
}
|
|
196
|
+
phaseSet.add(phase);
|
|
197
|
+
}
|
|
198
|
+
const gates = Array.isArray(manifest.gates) ? manifest.gates : [];
|
|
199
|
+
const seenGateIds = new Set();
|
|
200
|
+
gates.forEach((gate, index) => lintGate(gate, index, phaseSet, seenGateIds, options.allowCommands === true, findings));
|
|
201
|
+
const guards = Array.isArray(manifest.guards) ? manifest.guards : [];
|
|
202
|
+
guards.forEach((guard, index) => lintGuard(guard, index, phaseSet, findings));
|
|
203
|
+
return {
|
|
204
|
+
ok: findings.every((finding) => finding.severity !== 'error'),
|
|
205
|
+
id: options.id,
|
|
206
|
+
manifestPath,
|
|
207
|
+
gateCount: gates.length,
|
|
208
|
+
gateIds: [...seenGateIds],
|
|
209
|
+
findings
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-authored SOP (standard operating procedure) skill types — Feature A.
|
|
3
|
+
*
|
|
4
|
+
* A SOP is a user-defined workflow: ordered phases plus gates that guard entry
|
|
5
|
+
* into a phase. Gates are first-class, addressable objects (stable id) so a
|
|
6
|
+
* later metering layer (Feature B) can count them; Slice 1 only models and
|
|
7
|
+
* validates them — no registry, no enforcement.
|
|
8
|
+
*/
|
|
9
|
+
export type SopGateCheckType = 'file-exists' | 'grep' | 'command';
|
|
10
|
+
export type SopGateCheck = {
|
|
11
|
+
type: 'file-exists';
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* grep: by default passes when `pattern` is FOUND in `file`. Set `absent: true`
|
|
16
|
+
* to invert — pass when the pattern is NOT found. `absent` expresses "must not
|
|
17
|
+
* contain X" (e.g. no leftover TODO) as a pure-text check, with no command gate
|
|
18
|
+
* and no `--allow-commands` escalation.
|
|
19
|
+
*/
|
|
20
|
+
| {
|
|
21
|
+
type: 'grep';
|
|
22
|
+
file: string;
|
|
23
|
+
pattern: string;
|
|
24
|
+
absent?: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'command';
|
|
27
|
+
run: string[];
|
|
28
|
+
expectExitZero?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type SopGate = {
|
|
31
|
+
/** Stable id, unique within the SOP. Addressed by `peaks sop check --gate <id>`. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The phase whose entry this gate guards; must be one of the SOP's phases. */
|
|
34
|
+
phase: string;
|
|
35
|
+
check: SopGateCheck;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Binds a concrete irreversible Bash action to a phase: running a command that
|
|
39
|
+
* matches `bash` (a JS regex) counts as performing that phase's action, so the
|
|
40
|
+
* phase's gates must pass first. Enforced un-bypassably by the PreToolUse hook
|
|
41
|
+
* (`peaks gate enforce`). Optional — a SOP without guards still works exactly as
|
|
42
|
+
* before; enforcement is an opt-in overlay.
|
|
43
|
+
*/
|
|
44
|
+
export type SopPhaseGuard = {
|
|
45
|
+
/** The phase whose gates must pass before the matching command may run. Must be a declared phase. */
|
|
46
|
+
phase: string;
|
|
47
|
+
/** JS regular expression tested against the Bash `tool_input.command`. */
|
|
48
|
+
bash: string;
|
|
49
|
+
};
|
|
50
|
+
export type SopManifest = {
|
|
51
|
+
/** SOP id; namespaced separately from built-in peaks-* skills. */
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
/** Ordered, unique phase names. */
|
|
56
|
+
phases: string[];
|
|
57
|
+
gates: SopGate[];
|
|
58
|
+
/** Optional Bash-action guards enforced by the PreToolUse hook (opt-in). */
|
|
59
|
+
guards?: SopPhaseGuard[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A registered gate's workspace-level identity. `ref` (`<sopId>/<gateId>`) is
|
|
63
|
+
* unique across the workspace; `transition` (`<sopId>:<phase>`) is the binding
|
|
64
|
+
* a future enforcement layer (Slice 3) consults. Built-in peaks-* gates are
|
|
65
|
+
* never recorded here.
|
|
66
|
+
*/
|
|
67
|
+
export type RegisteredGate = {
|
|
68
|
+
ref: string;
|
|
69
|
+
gateId: string;
|
|
70
|
+
sopId: string;
|
|
71
|
+
phase: string;
|
|
72
|
+
transition: string;
|
|
73
|
+
};
|
|
74
|
+
export type RegisteredSop = {
|
|
75
|
+
id: string;
|
|
76
|
+
path: string;
|
|
77
|
+
gates: RegisteredGate[];
|
|
78
|
+
};
|
|
79
|
+
export type SopRegistry = {
|
|
80
|
+
version: 1;
|
|
81
|
+
sops: RegisteredSop[];
|
|
82
|
+
/** Total gate count across all registered SOPs — the workspace pool a metering layer would read. */
|
|
83
|
+
gateCount: number;
|
|
84
|
+
};
|
|
85
|
+
export type SopCheckResult = 'pass' | 'fail' | 'blocked';
|
|
86
|
+
export declare const SOP_GATE_CHECK_TYPES: ReadonlyArray<SopGateCheckType>;
|
|
87
|
+
/** SOP id grammar: lowercase kebab, must start alphanumeric. */
|
|
88
|
+
export declare const SOP_ID_PATTERN: RegExp;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-authored SOP (standard operating procedure) skill types — Feature A.
|
|
3
|
+
*
|
|
4
|
+
* A SOP is a user-defined workflow: ordered phases plus gates that guard entry
|
|
5
|
+
* into a phase. Gates are first-class, addressable objects (stable id) so a
|
|
6
|
+
* later metering layer (Feature B) can count them; Slice 1 only models and
|
|
7
|
+
* validates them — no registry, no enforcement.
|
|
8
|
+
*/
|
|
9
|
+
export const SOP_GATE_CHECK_TYPES = ['file-exists', 'grep', 'command'];
|
|
10
|
+
/** SOP id grammar: lowercase kebab, must start alphanumeric. */
|
|
11
|
+
export const SOP_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -2,5 +2,5 @@ export declare const repoRoot: string;
|
|
|
2
2
|
export declare const skillsDir: string;
|
|
3
3
|
export declare const schemasDir: string;
|
|
4
4
|
export declare const templatesDir: string;
|
|
5
|
-
export declare const requiredSkillNames: readonly ["peaks-solo", "peaks-prd", "peaks-ui", "peaks-rd", "peaks-qa", "peaks-sc", "peaks-txt"];
|
|
5
|
+
export declare const requiredSkillNames: readonly ["peaks-solo", "peaks-prd", "peaks-ui", "peaks-rd", "peaks-qa", "peaks-sc", "peaks-txt", "peaks-sop"];
|
|
6
6
|
export declare const requiredSchemaFiles: readonly ["artifact-manifest.schema.json", "context-capsule.schema.json", "approval-record.schema.json", "change-impact.schema.json", "refactor-slice-spec.schema.json", "artifact-retention-report.schema.json", "capability-source.schema.json", "capability-item.schema.json", "capability-availability.schema.json", "recommendation-plan.schema.json", "artifact-workspace.schema.json", "mcp-server.schema.json", "mcp-install-spec.schema.json", "mcp-install-plan.schema.json", "mcp-apply-result.schema.json", "openspec-change-summary.schema.json", "openspec-render-request.schema.json", "openspec-validation-result.schema.json", "doctor-report.schema.json"];
|
package/dist/src/shared/paths.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.
|
|
1
|
+
export declare const CLI_VERSION = "1.2.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.
|
|
1
|
+
export const CLI_VERSION = "1.2.0";
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Peaks SOP Manifest",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["id", "name", "phases", "gates"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
|
8
|
+
"name": { "type": "string" },
|
|
9
|
+
"description": { "type": "string" },
|
|
10
|
+
"phases": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 1,
|
|
13
|
+
"items": { "type": "string" }
|
|
14
|
+
},
|
|
15
|
+
"guards": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"description": "Optional Bash-action guards enforced by the PreToolUse hook (peaks gate enforce).",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["phase", "bash"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"phase": { "type": "string" },
|
|
23
|
+
"bash": { "type": "string", "description": "JS regex tested against the Bash tool_input.command" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"gates": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["id", "phase", "check"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
|
34
|
+
"phase": { "type": "string" },
|
|
35
|
+
"check": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["type"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"type": { "type": "string", "enum": ["file-exists", "grep", "command"] },
|
|
40
|
+
"path": { "type": "string" },
|
|
41
|
+
"file": { "type": "string" },
|
|
42
|
+
"pattern": { "type": "string" },
|
|
43
|
+
"absent": { "type": "boolean", "description": "grep only: pass when the pattern is NOT found (\"must not contain X\")" },
|
|
44
|
+
"run": { "type": "array", "items": { "type": "string" } },
|
|
45
|
+
"expectExitZero": { "type": "boolean" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peaks-sop
|
|
3
|
+
description: Authoring skill for user-defined SOPs (standard operating procedures) in Peaks. Use when a user wants to create, edit, debug, or register their own gated workflow — ordered phases plus gates that block advancement until conditions are met — by describing it in natural language instead of hand-writing JSON or memorizing CLI commands. DOMAIN-AGNOSTIC: not just software/release flows — equally for content publishing, compliance and approval checklists, data pipelines, onboarding, ops runbooks, or any personal repeatable procedure, wherever "don't enter the next stage until X is true" applies and X is checkable via a file, file content, or a command.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Peaks-Cli SOP Authoring
|
|
7
|
+
|
|
8
|
+
Peaks-Cli SOP turns a natural-language workflow description into a validated, registered custom SOP, then helps the user debug it until each gate behaves as intended. The user describes the process in plain language; this skill drives the `peaks sop` CLI on their behalf — they never have to hand-write `sop.json` or remember the command sequence.
|
|
9
|
+
|
|
10
|
+
**This is a general workflow-gating tool, not a developer-only tool.** A SOP is any repeatable process with ordered stages where you must not skip ahead until conditions are met. Software release is just one example; content publishing, compliance/approval checklists, data validation pipelines, employee onboarding, ops runbooks, and personal procedures are all first-class — often the more valuable use. When you interview the user, do not assume code: ask about *their* process in *their* domain.
|
|
11
|
+
|
|
12
|
+
## What to tell the user BEFORE running any command
|
|
13
|
+
|
|
14
|
+
This skill helps you create, test, and enforce a repeatable workflow (a "SOP")
|
|
15
|
+
with gates that physically block advancement until conditions are met. It works
|
|
16
|
+
for ANY domain — content publishing, compliance, onboarding, data pipelines,
|
|
17
|
+
software releases, personal procedures.
|
|
18
|
+
|
|
19
|
+
Before you run `peaks skill presence:set` or any other setup command, tell the
|
|
20
|
+
user (in your own words, one or two sentences):
|
|
21
|
+
|
|
22
|
+
"I'll help you turn your process into a Peaks SOP — ordered stages plus gates
|
|
23
|
+
that make sure you never skip a step. I'll interview you about your workflow,
|
|
24
|
+
generate the definition, test it, and register it. Ready?"
|
|
25
|
+
|
|
26
|
+
If the user has never used Peaks before, offer a 30-second demo:
|
|
27
|
+
|
|
28
|
+
"Want me to create a quick demo SOP first? It takes 30 seconds — I'll
|
|
29
|
+
scaffold one, show you how a gate blocks, then we can replace it with your
|
|
30
|
+
real workflow."
|
|
31
|
+
|
|
32
|
+
When they accept, use `demo-sop` as the id (NOT `peaks-sop` — the `peaks-` prefix
|
|
33
|
+
is reserved), name it "Demo SOP", make two phases (draft → done), one gate
|
|
34
|
+
(file-exists README.md), then run the full init→lint→check→register→hooks
|
|
35
|
+
install flow so they see the whole thing. Then offer to replace it with their
|
|
36
|
+
real process.
|
|
37
|
+
|
|
38
|
+
## Skill presence (MANDATORY first action — run AFTER telling the user what you're doing)
|
|
39
|
+
|
|
40
|
+
If the user invoked this skill directly ("peaks-sop") without mentioning a
|
|
41
|
+
specific project, assume `--project .` (current directory). If they are NOT in
|
|
42
|
+
the middle of a multi-skill peaks-solo/prd/rd/qa pipeline, skip `statusline
|
|
43
|
+
install` — the status bar is for long-running orchestrations; a standalone SOP
|
|
44
|
+
authoring session does not need it.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
peaks skill presence:set peaks-sop --project <repo> --mode <mode> --gate startup
|
|
48
|
+
peaks project memories --project <repo> --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then display: `Peaks-Cli Skill: peaks-sop | Peaks-Cli Gate: startup | Next: <one short action>`. Update gates with `peaks skill presence:set peaks-sop --project <repo> --mode <mode> --gate <gate>` when they change. When the SOP is registered and the user is satisfied, run `peaks skill presence:clear --project <repo>`.
|
|
52
|
+
|
|
53
|
+
## Responsibilities
|
|
54
|
+
|
|
55
|
+
- interview the user to turn a natural-language workflow into ordered **phases** and the **gates** that guard entry into each phase;
|
|
56
|
+
- generate a valid SOP manifest (`sop.json`) and the registrable `SKILL.md` via the CLI — never make the user hand-author JSON;
|
|
57
|
+
- run the lint → fix → re-lint debug loop until the manifest is clean;
|
|
58
|
+
- test each gate (pass/fail/blocked) and dry-run advancement so the user sees the SOP behave before committing;
|
|
59
|
+
- register the SOP so it joins skill presence / statusline like a built-in skill;
|
|
60
|
+
- explain the three gate types and the security posture of command gates.
|
|
61
|
+
|
|
62
|
+
## What a SOP is (explain this to the user)
|
|
63
|
+
|
|
64
|
+
A SOP is an **ordered list of phases** plus **gates** bound to phases. A gate that does not pass blocks advancement into its phase — this is how "don't drop steps" applies to the user's own workflow. The SOP lives at `.peaks/sops/<sop-id>/` (manifest `sop.json` + a registrable `SKILL.md`). Gate checks come in three types:
|
|
65
|
+
|
|
66
|
+
| type | fields | passes when |
|
|
67
|
+
|------|--------|-------------|
|
|
68
|
+
| `file-exists` | `path` | the file exists (path pinned inside the project) |
|
|
69
|
+
| `grep` | `file` + `pattern` (+ `absent`) | the regex matches in the file — or, with `absent: true`, does NOT match |
|
|
70
|
+
| `command` | `run` (argv array) + `expectExitZero` | the command exits as expected |
|
|
71
|
+
|
|
72
|
+
Prefer **`grep` with `absent: true`** for any "must not contain X" gate (no leftover `TODO`, no placeholder, no unresolved marker). It is a pure-text check — no `--allow-commands`, cross-platform, no shell. Reach for a `command` gate only when the check genuinely needs to run a program.
|
|
73
|
+
|
|
74
|
+
`command` gates run user-defined processes and are **refused by default** — they require explicit `--allow-commands`, run with no shell (argv array, no injection), a timeout, and cwd pinned to the project. Always tell the user when a SOP needs `--allow-commands` and why.
|
|
75
|
+
|
|
76
|
+
### Where SOP files live (two definition layers, per-project execution)
|
|
77
|
+
|
|
78
|
+
A SOP *definition* (manifest + SKILL.md) can live in two layers:
|
|
79
|
+
- **Global** `~/.peaks/sops/<sop-id>/` — your personal SOPs, reusable across every project. `init` / `lint` / `register` default here (no `--project`).
|
|
80
|
+
- **Project** `<project>/.peaks/sops/<sop-id>/` — **committed into the repo**, so a teammate who clones it gets the SOP (and, with the hook installed, is enforced). Pass `--project <repo>` to `init` / `lint` / `register` to use this layer.
|
|
81
|
+
|
|
82
|
+
The **project layer takes precedence** over global for the same id. Execution reads see the merged view (project wins). A SOP's *run-state* (current phase, history) is always **per-project**: `<project>/.peaks/sop-state/<sop-id>/`. `check` / `advance` take `--project` (default: current directory) to say which project the gates evaluate against, whose progress advances, and which definition layer wins.
|
|
83
|
+
|
|
84
|
+
`advance` also enforces **phase order**: you may re-enter the current phase, step back, or move to the immediately-next phase, but not skip ahead — a forward jump returns `SOP_PHASE_SKIP` (bypassable, like a gate, with `--allow-incomplete --reason`).
|
|
85
|
+
|
|
86
|
+
### Where SOPs apply (lead with the user's domain, not code)
|
|
87
|
+
|
|
88
|
+
The three gate primitives are domain-neutral, so the same engine governs very different workflows:
|
|
89
|
+
|
|
90
|
+
| domain | phases (example) | gate idea |
|
|
91
|
+
|--------|------------------|-----------|
|
|
92
|
+
| content / publishing | draft → edit → publish | `file-exists` the draft; `grep` no `TODO`/`TKTK`; `command` runs a spell/word-count check |
|
|
93
|
+
| compliance / approval | prepare → review → sign-off | `file-exists` `approval.md`; `grep` the doc contains "Approved" |
|
|
94
|
+
| data pipeline | raw → cleaned → validated | `command` runs a validator script that exits 0 |
|
|
95
|
+
| onboarding / ops | request → provision → done | `file-exists` each checklist artifact; `command` verifies a config |
|
|
96
|
+
| personal procedure | any repeatable steps | whatever "don't forget step X" means, expressed as a file/grep/command |
|
|
97
|
+
|
|
98
|
+
The one boundary to explain: a gate must reduce to **a file existing, text matching in a file, or a command's exit code**. A purely human-judgment gate ("did the editor approve?") is expressed by reifying it into a signal — e.g. require an `approved.md` file, or that a status file contains "approved". The `command` gate is the universal adapter for anything scriptable.
|
|
99
|
+
|
|
100
|
+
## Un-bypassable enforcement (optional, opt-in)
|
|
101
|
+
|
|
102
|
+
By default a SOP gate only blocks the `peaks sop advance` command — nothing forces the agent through it. To make a gate **physically un-bypassable**, a SOP can declare **guards** that bind a concrete irreversible Bash action to a phase, and the user installs a PreToolUse hook:
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
// in sop.json
|
|
106
|
+
"guards": [ { "phase": "publish", "bash": "git +push" } ]
|
|
107
|
+
```
|
|
108
|
+
Meaning: running a Bash command matching `git +push` IS entering the publish phase, so publish's gates must pass first. Then:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
peaks hooks install --project <repo> # explicit, opt-in; writes one PreToolUse entry
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Now when the agent tries `git push` while the publish gate fails, Claude Code receives `permissionDecision: "deny"` and the command is blocked **before any permission check — it holds even under `--dangerously-skip-permissions`**. CI only blocks at merge; CLAUDE.md instructions are advisory; this blocks in-conversation and cannot be skipped.
|
|
115
|
+
|
|
116
|
+
- `bash` is a **JS regex inside JSON** — escape backslashes (`"git\\s+push"`) or just use `"git +push"`. `peaks sop lint` rejects an invalid regex (`GUARD_INVALID_PATTERN`).
|
|
117
|
+
- Emergency override: `peaks gate bypass --sop <id> --phase <phase> --reason "<why>"` records a **one-shot** token consumed by the next blocked command (capped per project per SOP, reason audited).
|
|
118
|
+
- Trust: enforcement **fails open** — any internal error allows the command (a Peaks bug never bricks Claude Code); only a real gate failure denies. Installing the hook is an explicit user command; this skill never writes `settings.json` itself.
|
|
119
|
+
- `peaks hooks status` / `peaks hooks uninstall` manage the hook.
|
|
120
|
+
|
|
121
|
+
> Team enforcement: register the SOP into the **project layer** (`peaks sop init/register --project <repo>`) so the definition is committed in the repo. A teammate who clones it — even with an empty global `~/.peaks` — is enforced by the same gates once they install the hook. (A SOP that lives only in your global `~/.peaks` enforces only on your machine.)
|
|
122
|
+
|
|
123
|
+
## Default runbook
|
|
124
|
+
|
|
125
|
+
The default sequence this skill executes on the user's behalf. The natural-language → generate → debug loop IS this runbook.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# self-check (required by doctor; no user-visible effect)
|
|
129
|
+
peaks skill runbook peaks-sop --json
|
|
130
|
+
|
|
131
|
+
# 1. interview the user FIRST (before any scaffold): what are the ordered stages?
|
|
132
|
+
# For each stage, what must be true before entering it? Translate answers into
|
|
133
|
+
# file-exists / grep / grep-absent / command gates.
|
|
134
|
+
|
|
135
|
+
# 2. scaffold the SOP based on the interview (preview first, then apply)
|
|
136
|
+
# definitions are global (~/.peaks/sops) — init/lint/register take no --project
|
|
137
|
+
peaks sop init --id <sop-id> --name "<human name>" --json
|
|
138
|
+
peaks sop init --id <sop-id> --name "<human name>" --apply --json
|
|
139
|
+
|
|
140
|
+
# 3. write the phases/gates from the interview into ~/.peaks/sops/<sop-id>/sop.json
|
|
141
|
+
# (edit the manifest directly — the user described it in natural language)
|
|
142
|
+
|
|
143
|
+
# 4. DEBUG LOOP: lint, fix the reported findings, re-lint until clean
|
|
144
|
+
peaks sop lint --id <sop-id> --json
|
|
145
|
+
peaks sop lint --id <sop-id> --allow-commands --json # when the SOP uses command gates
|
|
146
|
+
|
|
147
|
+
# 5. test each gate behaves as intended (pass / fail / blocked) against a project
|
|
148
|
+
peaks sop check --id <sop-id> --gate <gate-id> --project <repo> --json
|
|
149
|
+
|
|
150
|
+
# 6. dry-run the flow to confirm gates + phase order block/allow the right phases
|
|
151
|
+
peaks sop advance --id <sop-id> --to <phase> --project <repo> --dry-run --json
|
|
152
|
+
|
|
153
|
+
# 7. register the SOP (preview, then apply) so it joins presence/statusline
|
|
154
|
+
peaks sop register --id <sop-id> --dry-run --json
|
|
155
|
+
peaks sop register --id <sop-id> --json
|
|
156
|
+
peaks sop registry --json
|
|
157
|
+
|
|
158
|
+
# 8. (optional) make a gate un-bypassable: declare guards in sop.json, then install the hook
|
|
159
|
+
peaks hooks install --project <repo>
|
|
160
|
+
peaks hooks status --project <repo>
|
|
161
|
+
# emergency one-shot override when a guarded action must proceed despite a failing gate:
|
|
162
|
+
peaks gate bypass --sop <sop-id> --phase <phase> --reason "<why>" --project <repo>
|
|
163
|
+
|
|
164
|
+
# 9. hand the SOP to the user; clear presence when done
|
|
165
|
+
peaks skill presence:clear --project <repo>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Transition verification gates (MANDATORY — run the command, see the output)
|
|
169
|
+
|
|
170
|
+
You cannot declare a SOP ready from memory. Each gate below is a command you **MUST run** and whose output you **MUST see**.
|
|
171
|
+
|
|
172
|
+
**Peaks-Cli Gate A — the manifest lints clean before register:**
|
|
173
|
+
```bash
|
|
174
|
+
peaks sop lint --id <sop-id> --json
|
|
175
|
+
# Expected: ok:true. If ok:false, fix the findings and re-lint — do NOT register a SOP that does not lint.
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Peaks-Cli Gate B — gates behave as intended before handing off:**
|
|
179
|
+
```bash
|
|
180
|
+
peaks sop check --id <sop-id> --gate <gate-id> --project <repo> --json
|
|
181
|
+
# Confirm each gate returns the verdict the user expects (pass on the good state, fail/blocked otherwise).
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Debugging guidance
|
|
185
|
+
|
|
186
|
+
When `sop lint` reports findings, fix them in `sop.json` and re-lint — common findings: duplicate gate id, gate bound to an undefined phase, missing check fields, or a command gate without `--allow-commands`. When `sop check` returns `blocked`, the check could not be evaluated (path escaped the project, target file unreadable, command not permitted or failed to spawn) — distinct from `fail` (evaluated, condition not met). Use `sop advance --dry-run` to confirm the blocking behaves before any real advance.
|
|
187
|
+
|
|
188
|
+
Concrete manifest reference, gate cookbook, and the debug loop: `references/sop-authoring.md`.
|
|
189
|
+
|
|
190
|
+
## Boundaries
|
|
191
|
+
|
|
192
|
+
Do not implement the user's business code, run their real release, or modify runtime configuration. This skill authors and validates the SOP definition; the SOP's gates then govern the user's own workflow. `command` gates execute user-defined commands only with explicit `--allow-commands` consent. Do not register a SOP that does not lint clean.
|