pumuki 6.3.13 → 6.3.14

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 (49) hide show
  1. package/README.md +95 -7
  2. package/VERSION +1 -1
  3. package/bin/pumuki-mcp-enterprise.js +5 -0
  4. package/bin/pumuki-pre-write.js +11 -0
  5. package/docs/API_REFERENCE.md +2 -1
  6. package/docs/INSTALLATION.md +101 -54
  7. package/docs/MCP_SERVERS.md +167 -74
  8. package/docs/PUMUKI_FULL_VALIDATION_CHECKLIST.md +46 -45
  9. package/docs/PUMUKI_OPENSPEC_SDD_ROADMAP.md +55 -0
  10. package/docs/README.md +5 -0
  11. package/docs/REFRACTOR_PROGRESS.md +102 -3
  12. package/docs/USAGE.md +115 -8
  13. package/docs/validation/README.md +2 -0
  14. package/docs/validation/phase12-go-no-go-report.md +73 -0
  15. package/docs/validation/post-phase12-next-lot-decision.md +75 -0
  16. package/integrations/config/skillsRuleSet.ts +53 -6
  17. package/integrations/evidence/buildEvidence.ts +42 -3
  18. package/integrations/evidence/generateEvidence.test.ts +59 -0
  19. package/integrations/evidence/readEvidence.test.ts +61 -0
  20. package/integrations/evidence/schema.test.ts +81 -0
  21. package/integrations/evidence/schema.ts +11 -0
  22. package/integrations/evidence/writeEvidence.test.ts +18 -0
  23. package/integrations/evidence/writeEvidence.ts +11 -0
  24. package/integrations/git/resolveGitRefs.ts +2 -2
  25. package/integrations/git/runPlatformGate.ts +64 -0
  26. package/integrations/git/runPlatformGateEvidence.ts +13 -0
  27. package/integrations/git/stageRunners.ts +10 -1
  28. package/integrations/lifecycle/artifacts.ts +57 -4
  29. package/integrations/lifecycle/cli.ts +248 -12
  30. package/integrations/lifecycle/constants.ts +1 -0
  31. package/integrations/lifecycle/gitService.ts +1 -0
  32. package/integrations/lifecycle/install.ts +24 -1
  33. package/integrations/lifecycle/openSpecBootstrap.ts +190 -0
  34. package/integrations/lifecycle/state.ts +57 -0
  35. package/integrations/lifecycle/uninstall.ts +3 -1
  36. package/integrations/lifecycle/update.ts +11 -0
  37. package/integrations/mcp/enterpriseServer.cli.ts +12 -0
  38. package/integrations/mcp/enterpriseServer.ts +762 -0
  39. package/integrations/mcp/index.ts +1 -0
  40. package/integrations/sdd/index.ts +11 -0
  41. package/integrations/sdd/openSpecCli.ts +180 -0
  42. package/integrations/sdd/policy.ts +190 -0
  43. package/integrations/sdd/sessionStore.ts +152 -0
  44. package/integrations/sdd/types.ts +69 -0
  45. package/package.json +10 -4
  46. package/scripts/framework-menu-runner-path-lib.ts +10 -3
  47. package/scripts/framework-menu.ts +86 -5
  48. package/scripts/package-install-smoke-gate-lib.ts +6 -1
  49. package/scripts/package-install-smoke-lifecycle-lib.ts +3 -0
@@ -1 +1,2 @@
1
1
  export { startEvidenceContextServer } from './evidenceContextServer';
2
+ export { startEnterpriseMcpServer } from './enterpriseServer';
@@ -0,0 +1,11 @@
1
+ export type {
2
+ OpenSpecValidationSummary,
3
+ SddDecision,
4
+ SddDecisionCode,
5
+ SddEvaluateResult,
6
+ SddSessionState,
7
+ SddStage,
8
+ SddStatusPayload,
9
+ } from './types';
10
+ export { evaluateSddPolicy, readSddStatus } from './policy';
11
+ export { closeSddSession, openSddSession, readSddSession, refreshSddSession } from './sessionStore';
@@ -0,0 +1,180 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import type { OpenSpecValidationSummary } from './types';
5
+
6
+ export const OPENSPEC_NPM_PACKAGE_NAME = '@fission-ai/openspec';
7
+ export const OPENSPEC_COMPATIBILITY_MATRIX = {
8
+ packageName: OPENSPEC_NPM_PACKAGE_NAME,
9
+ minimumVersion: '1.1.1',
10
+ recommendedVersion: '1.1.1',
11
+ } as const;
12
+
13
+ type OpenSpecCommandResult = {
14
+ exitCode: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ };
18
+
19
+ const resolveOpenSpecBinary = (repoRoot: string): string => {
20
+ const binaryName = process.platform === 'win32' ? 'openspec.cmd' : 'openspec';
21
+ const localBinaryPath = join(repoRoot, 'node_modules', '.bin', binaryName);
22
+ if (existsSync(localBinaryPath)) {
23
+ return localBinaryPath;
24
+ }
25
+ return 'openspec';
26
+ };
27
+
28
+ const runOpenSpecCommand = (
29
+ args: ReadonlyArray<string>,
30
+ cwd: string
31
+ ): OpenSpecCommandResult => {
32
+ const result = spawnSync(resolveOpenSpecBinary(cwd), [...args], {
33
+ cwd,
34
+ encoding: 'utf8',
35
+ });
36
+ return {
37
+ exitCode: typeof result.status === 'number' ? result.status : 1,
38
+ stdout: result.stdout ?? '',
39
+ stderr: result.stderr ?? '',
40
+ };
41
+ };
42
+
43
+ const extractSemver = (raw: string | undefined): string | undefined => {
44
+ if (typeof raw !== 'string') {
45
+ return undefined;
46
+ }
47
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
48
+ if (!match) {
49
+ return undefined;
50
+ }
51
+ return `${match[1]}.${match[2]}.${match[3]}`;
52
+ };
53
+
54
+ const compareSemver = (left: string, right: string): number => {
55
+ const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
56
+ const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
57
+ for (let index = 0; index < 3; index += 1) {
58
+ const leftPart = leftParts[index] ?? 0;
59
+ const rightPart = rightParts[index] ?? 0;
60
+ if (leftPart > rightPart) {
61
+ return 1;
62
+ }
63
+ if (leftPart < rightPart) {
64
+ return -1;
65
+ }
66
+ }
67
+ return 0;
68
+ };
69
+
70
+ export type OpenSpecCompatibilityStatus = {
71
+ minimumVersion: string;
72
+ recommendedVersion: string;
73
+ detectedVersion?: string;
74
+ parsedVersion?: string;
75
+ compatible: boolean;
76
+ };
77
+
78
+ export const evaluateOpenSpecCompatibility = (params: {
79
+ installed: boolean;
80
+ version?: string;
81
+ }): OpenSpecCompatibilityStatus => {
82
+ const parsedVersion = extractSemver(params.version);
83
+ if (!params.installed || !parsedVersion) {
84
+ return {
85
+ minimumVersion: OPENSPEC_COMPATIBILITY_MATRIX.minimumVersion,
86
+ recommendedVersion: OPENSPEC_COMPATIBILITY_MATRIX.recommendedVersion,
87
+ detectedVersion: params.version,
88
+ parsedVersion,
89
+ compatible: false,
90
+ };
91
+ }
92
+
93
+ return {
94
+ minimumVersion: OPENSPEC_COMPATIBILITY_MATRIX.minimumVersion,
95
+ recommendedVersion: OPENSPEC_COMPATIBILITY_MATRIX.recommendedVersion,
96
+ detectedVersion: params.version,
97
+ parsedVersion,
98
+ compatible:
99
+ compareSemver(parsedVersion, OPENSPEC_COMPATIBILITY_MATRIX.minimumVersion) >= 0,
100
+ };
101
+ };
102
+
103
+ const parseValidationJson = (stdout: string): {
104
+ totals: { items: number; failed: number; passed: number };
105
+ issues: { errors: number; warnings: number; infos: number };
106
+ } => {
107
+ try {
108
+ const parsed: unknown = JSON.parse(stdout);
109
+ const summary = (parsed as { summary?: unknown }).summary;
110
+ const totals = (summary as { totals?: unknown })?.totals as
111
+ | { items?: unknown; failed?: unknown; passed?: unknown }
112
+ | undefined;
113
+ const byLevel = (summary as { byLevel?: unknown })?.byLevel as
114
+ | { ERROR?: unknown; WARNING?: unknown; INFO?: unknown }
115
+ | undefined;
116
+
117
+ return {
118
+ totals: {
119
+ items: Number(totals?.items ?? 0) || 0,
120
+ failed: Number(totals?.failed ?? 0) || 0,
121
+ passed: Number(totals?.passed ?? 0) || 0,
122
+ },
123
+ issues: {
124
+ errors: Number(byLevel?.ERROR ?? 0) || 0,
125
+ warnings: Number(byLevel?.WARNING ?? 0) || 0,
126
+ infos: Number(byLevel?.INFO ?? 0) || 0,
127
+ },
128
+ };
129
+ } catch {
130
+ return {
131
+ totals: {
132
+ items: 0,
133
+ failed: 0,
134
+ passed: 0,
135
+ },
136
+ issues: {
137
+ errors: 0,
138
+ warnings: 0,
139
+ infos: 0,
140
+ },
141
+ };
142
+ }
143
+ };
144
+
145
+ export const detectOpenSpecInstallation = (
146
+ repoRoot: string
147
+ ): { installed: boolean; version?: string } => {
148
+ const result = runOpenSpecCommand(['--version'], repoRoot);
149
+ if (result.exitCode !== 0) {
150
+ return {
151
+ installed: false,
152
+ };
153
+ }
154
+ return {
155
+ installed: true,
156
+ version: result.stdout.trim() || undefined,
157
+ };
158
+ };
159
+
160
+ export const isOpenSpecProjectInitialized = (repoRoot: string): boolean =>
161
+ existsSync(resolve(repoRoot, 'openspec'));
162
+
163
+ export const validateOpenSpecChanges = (
164
+ repoRoot: string
165
+ ): OpenSpecValidationSummary => {
166
+ const result = runOpenSpecCommand(
167
+ ['validate', '--changes', '--json', '--no-interactive'],
168
+ repoRoot
169
+ );
170
+ const parsed = parseValidationJson(result.stdout);
171
+ const hasErrors = parsed.totals.failed > 0 || parsed.issues.errors > 0;
172
+ return {
173
+ ok: result.exitCode === 0 && !hasErrors,
174
+ exitCode: result.exitCode,
175
+ stdout: result.stdout,
176
+ stderr: result.stderr,
177
+ totals: parsed.totals,
178
+ issues: parsed.issues,
179
+ };
180
+ };
@@ -0,0 +1,190 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import {
4
+ detectOpenSpecInstallation,
5
+ evaluateOpenSpecCompatibility,
6
+ isOpenSpecProjectInitialized,
7
+ validateOpenSpecChanges,
8
+ } from './openSpecCli';
9
+ import { readSddSession } from './sessionStore';
10
+ import type {
11
+ SddDecision,
12
+ SddEvaluateResult,
13
+ SddStage,
14
+ SddStatusPayload,
15
+ } from './types';
16
+
17
+ const buildStatus = (repoRoot: string): SddStatusPayload => {
18
+ const openspec = detectOpenSpecInstallation(repoRoot);
19
+ const compatibility = evaluateOpenSpecCompatibility(openspec);
20
+ const session = readSddSession(repoRoot);
21
+ return {
22
+ repoRoot,
23
+ openspec: {
24
+ installed: openspec.installed,
25
+ version: openspec.version,
26
+ projectInitialized: isOpenSpecProjectInitialized(repoRoot),
27
+ minimumVersion: compatibility.minimumVersion,
28
+ recommendedVersion: compatibility.recommendedVersion,
29
+ compatible: compatibility.compatible,
30
+ parsedVersion: compatibility.parsedVersion,
31
+ },
32
+ session,
33
+ };
34
+ };
35
+
36
+ const allowed = (message: string, details?: Record<string, unknown>): SddDecision => ({
37
+ allowed: true,
38
+ code: 'ALLOWED',
39
+ message,
40
+ details,
41
+ });
42
+
43
+ const blocked = (
44
+ code: Exclude<SddDecision['code'], 'ALLOWED'>,
45
+ message: string,
46
+ details?: Record<string, unknown>
47
+ ): SddDecision => ({
48
+ allowed: false,
49
+ code,
50
+ message,
51
+ details,
52
+ });
53
+
54
+ const activeChangeExists = (repoRoot: string, changeId: string): boolean =>
55
+ existsSync(resolve(repoRoot, 'openspec', 'changes', changeId));
56
+
57
+ const activeChangeArchived = (repoRoot: string, changeId: string): boolean =>
58
+ existsSync(resolve(repoRoot, 'openspec', 'changes', 'archive', changeId));
59
+
60
+ const evaluateSessionRequirements = (status: SddStatusPayload): SddDecision | undefined => {
61
+ if (!status.session.active) {
62
+ return blocked(
63
+ 'SDD_SESSION_MISSING',
64
+ 'SDD session is not active. Run `pumuki sdd session --open --change=<id>`.'
65
+ );
66
+ }
67
+ if (!status.session.valid || !status.session.changeId) {
68
+ return blocked(
69
+ 'SDD_SESSION_INVALID',
70
+ 'SDD session is invalid or expired. Run `pumuki sdd session --refresh` or reopen it.'
71
+ );
72
+ }
73
+ if (activeChangeArchived(status.repoRoot, status.session.changeId)) {
74
+ return blocked(
75
+ 'SDD_CHANGE_ARCHIVED',
76
+ `Active SDD change "${status.session.changeId}" is archived. Open a new active change session.`
77
+ );
78
+ }
79
+ if (!activeChangeExists(status.repoRoot, status.session.changeId)) {
80
+ return blocked(
81
+ 'SDD_CHANGE_MISSING',
82
+ `Active SDD change "${status.session.changeId}" was not found in openspec/changes.`
83
+ );
84
+ }
85
+ return undefined;
86
+ };
87
+
88
+ export const evaluateSddPolicy = (params: {
89
+ stage: SddStage;
90
+ repoRoot?: string;
91
+ }): SddEvaluateResult => {
92
+ const repoRoot = params.repoRoot ?? process.cwd();
93
+ const bypassEnabled = process.env.PUMUKI_SDD_BYPASS === '1';
94
+ const status = buildStatus(repoRoot);
95
+
96
+ if (bypassEnabled) {
97
+ return {
98
+ stage: params.stage,
99
+ status,
100
+ decision: allowed(
101
+ 'SDD bypass is active via PUMUKI_SDD_BYPASS=1. Enforcement skipped by emergency override.',
102
+ {
103
+ bypass: true,
104
+ env: 'PUMUKI_SDD_BYPASS',
105
+ }
106
+ ),
107
+ };
108
+ }
109
+
110
+ if (!status.openspec.installed) {
111
+ return {
112
+ stage: params.stage,
113
+ status,
114
+ decision: blocked(
115
+ 'OPENSPEC_MISSING',
116
+ 'OpenSpec is required but was not detected. Install OpenSpec before continuing.'
117
+ ),
118
+ };
119
+ }
120
+
121
+ if (!status.openspec.compatible) {
122
+ return {
123
+ stage: params.stage,
124
+ status,
125
+ decision: blocked(
126
+ 'OPENSPEC_VERSION_UNSUPPORTED',
127
+ `OpenSpec version is unsupported. Minimum required is ${status.openspec.minimumVersion} (detected: ${status.openspec.version ?? 'unknown'}).`
128
+ ),
129
+ };
130
+ }
131
+
132
+ if (!status.openspec.projectInitialized) {
133
+ return {
134
+ stage: params.stage,
135
+ status,
136
+ decision: blocked(
137
+ 'OPENSPEC_PROJECT_MISSING',
138
+ 'OpenSpec project is not initialized in this repository. Run OpenSpec init/bootstrap.'
139
+ ),
140
+ };
141
+ }
142
+
143
+ const sessionDecision = evaluateSessionRequirements(status);
144
+ if (sessionDecision) {
145
+ return {
146
+ stage: params.stage,
147
+ status,
148
+ decision: sessionDecision,
149
+ };
150
+ }
151
+
152
+ if (params.stage === 'PRE_WRITE') {
153
+ return {
154
+ stage: params.stage,
155
+ status,
156
+ decision: allowed('SDD pre-write checks passed with active valid session.'),
157
+ };
158
+ }
159
+
160
+ const validation = validateOpenSpecChanges(repoRoot);
161
+ if (!validation.ok) {
162
+ return {
163
+ stage: params.stage,
164
+ status,
165
+ validation,
166
+ decision: blocked(
167
+ validation.exitCode !== 0 ? 'SDD_VALIDATION_FAILED' : 'SDD_VALIDATION_ERROR',
168
+ 'OpenSpec validation failed for active changes. Resolve SDD issues before continuing.',
169
+ {
170
+ exitCode: validation.exitCode,
171
+ failedItems: validation.totals.failed,
172
+ errors: validation.issues.errors,
173
+ }
174
+ ),
175
+ };
176
+ }
177
+
178
+ return {
179
+ stage: params.stage,
180
+ status,
181
+ validation,
182
+ decision: allowed('SDD validation passed for active changes.', {
183
+ passedItems: validation.totals.passed,
184
+ warnings: validation.issues.warnings,
185
+ }),
186
+ };
187
+ };
188
+
189
+ export const readSddStatus = (repoRoot?: string): SddStatusPayload =>
190
+ buildStatus(repoRoot ?? process.cwd());
@@ -0,0 +1,152 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import {
4
+ LifecycleGitService,
5
+ type ILifecycleGitService,
6
+ } from '../lifecycle/gitService';
7
+ import type { SddSessionState } from './types';
8
+
9
+ const SDD_KEYS = {
10
+ active: 'pumuki.sdd.session.active',
11
+ change: 'pumuki.sdd.session.change',
12
+ updatedAt: 'pumuki.sdd.session.updatedAt',
13
+ expiresAt: 'pumuki.sdd.session.expiresAt',
14
+ ttlMinutes: 'pumuki.sdd.session.ttlMinutes',
15
+ } as const;
16
+
17
+ const DEFAULT_TTL_MINUTES = 45;
18
+
19
+ const resolveRepoRoot = (cwd: string, git: ILifecycleGitService): string =>
20
+ git.resolveRepoRoot(cwd);
21
+
22
+ const nowIso = (): string => new Date().toISOString();
23
+
24
+ const addMinutesIso = (minutes: number): string =>
25
+ new Date(Date.now() + minutes * 60_000).toISOString();
26
+
27
+ const parsePositiveMinutes = (value: number | undefined): number =>
28
+ Number.isFinite(value) && (value as number) > 0
29
+ ? Math.floor(value as number)
30
+ : DEFAULT_TTL_MINUTES;
31
+
32
+ const computeValidity = (expiresAt?: string): {
33
+ valid: boolean;
34
+ remainingSeconds?: number;
35
+ } => {
36
+ if (!expiresAt) {
37
+ return { valid: false };
38
+ }
39
+ const target = new Date(expiresAt).getTime();
40
+ if (!Number.isFinite(target)) {
41
+ return { valid: false };
42
+ }
43
+ const remaining = Math.floor((target - Date.now()) / 1000);
44
+ if (remaining <= 0) {
45
+ return { valid: false, remainingSeconds: 0 };
46
+ }
47
+ return { valid: true, remainingSeconds: remaining };
48
+ };
49
+
50
+ const readConfig = (
51
+ repoRoot: string,
52
+ git: ILifecycleGitService
53
+ ): SddSessionState => {
54
+ const active = git.getLocalConfig(repoRoot, SDD_KEYS.active) === 'true';
55
+ const changeId = git.getLocalConfig(repoRoot, SDD_KEYS.change) ?? undefined;
56
+ const updatedAt = git.getLocalConfig(repoRoot, SDD_KEYS.updatedAt) ?? undefined;
57
+ const expiresAt = git.getLocalConfig(repoRoot, SDD_KEYS.expiresAt) ?? undefined;
58
+ const ttlRaw = git.getLocalConfig(repoRoot, SDD_KEYS.ttlMinutes);
59
+ const ttlMinutes =
60
+ typeof ttlRaw === 'string' && ttlRaw.trim().length > 0
61
+ ? Number.parseInt(ttlRaw, 10)
62
+ : undefined;
63
+
64
+ const validity = computeValidity(expiresAt);
65
+ return {
66
+ repoRoot,
67
+ active,
68
+ changeId,
69
+ updatedAt,
70
+ expiresAt,
71
+ ttlMinutes,
72
+ valid: active && !!changeId && validity.valid,
73
+ remainingSeconds: validity.remainingSeconds,
74
+ };
75
+ };
76
+
77
+ const ensureChangePath = (repoRoot: string, changeId: string): {
78
+ exists: boolean;
79
+ archived: boolean;
80
+ } => {
81
+ const activePath = resolve(repoRoot, 'openspec', 'changes', changeId);
82
+ const archivedPath = resolve(repoRoot, 'openspec', 'changes', 'archive', changeId);
83
+ return {
84
+ exists: existsSync(activePath),
85
+ archived: existsSync(archivedPath),
86
+ };
87
+ };
88
+
89
+ export const readSddSession = (
90
+ cwd = process.cwd(),
91
+ git: ILifecycleGitService = new LifecycleGitService()
92
+ ): SddSessionState => {
93
+ const repoRoot = resolveRepoRoot(cwd, git);
94
+ return readConfig(repoRoot, git);
95
+ };
96
+
97
+ export const openSddSession = (params: {
98
+ changeId: string;
99
+ ttlMinutes?: number;
100
+ cwd?: string;
101
+ git?: ILifecycleGitService;
102
+ }): SddSessionState => {
103
+ const git = params.git ?? new LifecycleGitService();
104
+ const repoRoot = resolveRepoRoot(params.cwd ?? process.cwd(), git);
105
+ const changeId = params.changeId.trim();
106
+ const changeState = ensureChangePath(repoRoot, changeId);
107
+ if (!changeState.exists) {
108
+ throw new Error(`OpenSpec change "${changeId}" not found in openspec/changes.`);
109
+ }
110
+ if (changeState.archived) {
111
+ throw new Error(`OpenSpec change "${changeId}" is archived and cannot be used as active SDD session.`);
112
+ }
113
+
114
+ const ttlMinutes = parsePositiveMinutes(params.ttlMinutes);
115
+ git.setLocalConfig(repoRoot, SDD_KEYS.active, 'true');
116
+ git.setLocalConfig(repoRoot, SDD_KEYS.change, changeId);
117
+ git.setLocalConfig(repoRoot, SDD_KEYS.updatedAt, nowIso());
118
+ git.setLocalConfig(repoRoot, SDD_KEYS.expiresAt, addMinutesIso(ttlMinutes));
119
+ git.setLocalConfig(repoRoot, SDD_KEYS.ttlMinutes, String(ttlMinutes));
120
+ return readConfig(repoRoot, git);
121
+ };
122
+
123
+ export const refreshSddSession = (params?: {
124
+ ttlMinutes?: number;
125
+ cwd?: string;
126
+ git?: ILifecycleGitService;
127
+ }): SddSessionState => {
128
+ const git = params?.git ?? new LifecycleGitService();
129
+ const repoRoot = resolveRepoRoot(params?.cwd ?? process.cwd(), git);
130
+ const current = readConfig(repoRoot, git);
131
+ if (!current.active || !current.changeId) {
132
+ throw new Error('No active SDD session to refresh. Run `pumuki sdd session --open --change=<id>` first.');
133
+ }
134
+ const ttlMinutes = parsePositiveMinutes(params?.ttlMinutes ?? current.ttlMinutes);
135
+ git.setLocalConfig(repoRoot, SDD_KEYS.updatedAt, nowIso());
136
+ git.setLocalConfig(repoRoot, SDD_KEYS.expiresAt, addMinutesIso(ttlMinutes));
137
+ git.setLocalConfig(repoRoot, SDD_KEYS.ttlMinutes, String(ttlMinutes));
138
+ return readConfig(repoRoot, git);
139
+ };
140
+
141
+ export const closeSddSession = (
142
+ cwd = process.cwd(),
143
+ git: ILifecycleGitService = new LifecycleGitService()
144
+ ): SddSessionState => {
145
+ const repoRoot = resolveRepoRoot(cwd, git);
146
+ git.unsetLocalConfig(repoRoot, SDD_KEYS.active);
147
+ git.unsetLocalConfig(repoRoot, SDD_KEYS.change);
148
+ git.unsetLocalConfig(repoRoot, SDD_KEYS.updatedAt);
149
+ git.unsetLocalConfig(repoRoot, SDD_KEYS.expiresAt);
150
+ git.unsetLocalConfig(repoRoot, SDD_KEYS.ttlMinutes);
151
+ return readConfig(repoRoot, git);
152
+ };
@@ -0,0 +1,69 @@
1
+ export type SddStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
2
+
3
+ export type SddDecisionCode =
4
+ | 'ALLOWED'
5
+ | 'OPENSPEC_MISSING'
6
+ | 'OPENSPEC_VERSION_UNSUPPORTED'
7
+ | 'OPENSPEC_PROJECT_MISSING'
8
+ | 'SDD_SESSION_MISSING'
9
+ | 'SDD_SESSION_INVALID'
10
+ | 'SDD_CHANGE_MISSING'
11
+ | 'SDD_CHANGE_ARCHIVED'
12
+ | 'SDD_VALIDATION_FAILED'
13
+ | 'SDD_VALIDATION_ERROR';
14
+
15
+ export type SddDecision = {
16
+ allowed: boolean;
17
+ code: SddDecisionCode;
18
+ message: string;
19
+ details?: Record<string, unknown>;
20
+ };
21
+
22
+ export type OpenSpecValidationSummary = {
23
+ ok: boolean;
24
+ exitCode: number;
25
+ stdout: string;
26
+ stderr: string;
27
+ totals: {
28
+ items: number;
29
+ failed: number;
30
+ passed: number;
31
+ };
32
+ issues: {
33
+ errors: number;
34
+ warnings: number;
35
+ infos: number;
36
+ };
37
+ };
38
+
39
+ export type SddSessionState = {
40
+ repoRoot: string;
41
+ active: boolean;
42
+ changeId?: string;
43
+ updatedAt?: string;
44
+ expiresAt?: string;
45
+ ttlMinutes?: number;
46
+ valid: boolean;
47
+ remainingSeconds?: number;
48
+ };
49
+
50
+ export type SddStatusPayload = {
51
+ repoRoot: string;
52
+ openspec: {
53
+ installed: boolean;
54
+ version?: string;
55
+ projectInitialized: boolean;
56
+ minimumVersion: string;
57
+ recommendedVersion: string;
58
+ compatible: boolean;
59
+ parsedVersion?: string;
60
+ };
61
+ session: SddSessionState;
62
+ };
63
+
64
+ export type SddEvaluateResult = {
65
+ stage: SddStage;
66
+ decision: SddDecision;
67
+ status: SddStatusPayload;
68
+ validation?: OpenSpecValidationSummary;
69
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.13",
3
+ "version": "6.3.14",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,9 +9,11 @@
9
9
  "ast-hooks": "bin/pumuki-framework.js",
10
10
  "pumuki-framework": "bin/pumuki-framework.js",
11
11
  "pumuki-pre-commit": "bin/pumuki-pre-commit.js",
12
+ "pumuki-pre-write": "bin/pumuki-pre-write.js",
12
13
  "pumuki-pre-push": "bin/pumuki-pre-push.js",
13
14
  "pumuki-ci": "bin/pumuki-ci.js",
14
- "pumuki-mcp-evidence": "bin/pumuki-mcp-evidence.js"
15
+ "pumuki-mcp-evidence": "bin/pumuki-mcp-evidence.js",
16
+ "pumuki-mcp-enterprise": "bin/pumuki-mcp-enterprise.js"
15
17
  },
16
18
  "scripts": {
17
19
  "install-hooks": "node bin/pumuki.js install",
@@ -22,6 +24,7 @@
22
24
  "pumuki:update": "node bin/pumuki.js update --latest",
23
25
  "pumuki:doctor": "node bin/pumuki.js doctor",
24
26
  "pumuki:status": "node bin/pumuki.js status",
27
+ "pumuki:sdd:pre-write": "node bin/pumuki-pre-write.js",
25
28
  "audit": "node bin/ast",
26
29
  "ast": "node bin/ast",
27
30
  "audit-library": "node bin/audit-library.js",
@@ -37,14 +40,14 @@
37
40
  "violations:summary": "node bin/violations summary",
38
41
  "violations:top": "node bin/violations top",
39
42
  "violations:demo": "bash bin/demo-violations",
40
- "test": "npx --yes tsx@4.21.0 --test integrations/config/__tests__/*.test.ts integrations/gate/__tests__/*.test.ts integrations/git/__tests__/*.test.ts integrations/lifecycle/__tests__/*.test.ts scripts/__tests__/*.test.ts && jest --runInBand --testMatch \"**/__tests__/**/*.spec.ts\"",
43
+ "test": "npx --yes tsx@4.21.0 --test integrations/config/__tests__/*.test.ts integrations/gate/__tests__/*.test.ts integrations/git/__tests__/*.test.ts integrations/lifecycle/__tests__/*.test.ts integrations/sdd/__tests__/*.test.ts scripts/__tests__/*.test.ts && jest --runInBand --testMatch \"**/__tests__/**/*.spec.ts\"",
41
44
  "lint": "npm run typecheck",
42
45
  "build:ts": "tsc --noEmit",
43
46
  "typecheck": "tsc --noEmit",
44
47
  "test:heuristics": "npx --yes tsx@4.21.0 --test core/facts/__tests__/extractHeuristicFacts.test.ts",
45
48
  "test:evidence": "npx --yes tsx@4.21.0 --test integrations/evidence/__tests__/buildEvidence.test.ts integrations/evidence/__tests__/humanIntent.test.ts",
46
49
  "test:mcp": "npx --yes tsx@4.21.0 --test integrations/mcp/__tests__/*.test.ts",
47
- "test:stage-gates": "npx --yes tsx@4.21.0 --test integrations/config/__tests__/*.test.ts integrations/gate/__tests__/*.test.ts integrations/git/__tests__/*.test.ts integrations/lifecycle/__tests__/*.test.ts scripts/__tests__/*.test.ts",
50
+ "test:stage-gates": "npx --yes tsx@4.21.0 --test integrations/config/__tests__/*.test.ts integrations/gate/__tests__/*.test.ts integrations/git/__tests__/*.test.ts integrations/lifecycle/__tests__/*.test.ts integrations/sdd/__tests__/*.test.ts scripts/__tests__/*.test.ts",
48
51
  "test:deterministic": "npm run test:evidence && npm run test:mcp && npm run test:heuristics",
49
52
  "ast:refresh": "bash scripts/hooks-system/bin/update-evidence.sh",
50
53
  "ast:audit": "node scripts/hooks-system/infrastructure/ast/ast-intelligence.js",
@@ -103,6 +106,7 @@
103
106
  "skills:compile": "npx --yes tsx@4.21.0 scripts/compile-skills-lock.ts",
104
107
  "skills:lock:check": "npx --yes tsx@4.21.0 scripts/compile-skills-lock.ts --check",
105
108
  "mcp:evidence": "node bin/pumuki-mcp-evidence.js",
109
+ "mcp:enterprise": "node bin/pumuki-mcp-enterprise.js",
106
110
  "validate:adapter-hooks-local": "echo 'Migrated to modern TS scripts — use validation:adapter-readiness instead'",
107
111
  "verify:adapter-hooks-runtime": "echo 'Migrated to modern TS scripts — use validation:adapter-readiness instead'"
108
112
  },
@@ -185,6 +189,7 @@
185
189
  "integrations/gate/*.ts",
186
190
  "integrations/git/*.ts",
187
191
  "integrations/lifecycle/*.ts",
192
+ "integrations/sdd/*.ts",
188
193
  "integrations/mcp/*.ts",
189
194
  "integrations/platform/*.ts",
190
195
  "scripts/*.ts",
@@ -211,6 +216,7 @@
211
216
  "./package.json": "./package.json",
212
217
  "./integrations/git": "./integrations/git/index.ts",
213
218
  "./integrations/lifecycle": "./integrations/lifecycle/index.ts",
219
+ "./integrations/sdd": "./integrations/sdd/index.ts",
214
220
  "./integrations/mcp": "./integrations/mcp/index.ts",
215
221
  "./core/gate/evaluateGate": "./core/gate/evaluateGate.ts",
216
222
  "./core/gate/evaluateRules": "./core/gate/evaluateRules.ts"
@@ -2,9 +2,16 @@ import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  export const resolveScriptOrReportMissing = (relativePath: string): string | undefined => {
5
- const scriptPath = resolve(process.cwd(), relativePath);
6
- if (existsSync(scriptPath)) {
7
- return scriptPath;
5
+ const repoLocalPath = resolve(process.cwd(), relativePath);
6
+ if (existsSync(repoLocalPath)) {
7
+ return repoLocalPath;
8
+ }
9
+
10
+ // Consumer repos run menu binaries from node_modules, so scripts must also
11
+ // be resolved relative to the installed package root.
12
+ const packageFallbackPath = resolve(__dirname, '..', relativePath);
13
+ if (existsSync(packageFallbackPath)) {
14
+ return packageFallbackPath;
8
15
  }
9
16
 
10
17
  process.stdout.write(`\nCould not find ${relativePath} in current repository.\n`);