pumuki 6.3.170 → 6.3.171

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.
@@ -840,14 +840,6 @@ const hasSwiftXCTestOnlyBrownfieldSuiteUsage = (source: string): boolean => {
840
840
  return !hasSwiftTestingImportUsage(source) && !hasSwiftTestingSuiteAttributeUsage(source);
841
841
  };
842
842
 
843
- const hasSwiftXCTestHelperOrFactoryUsage = (source: string): boolean => {
844
- return (
845
- hasSwiftXCTestImportUsage(source) &&
846
- !hasSwiftXCTestCaseSubclassUsage(source) &&
847
- !hasSwiftLegacyXCTestMethodUsage(source)
848
- );
849
- };
850
-
851
843
  export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
852
844
  if (!hasSwiftXCTestImportUsage(source)) {
853
845
  return false;
@@ -893,10 +885,6 @@ export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
893
885
  return false;
894
886
  }
895
887
 
896
- if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
897
- return false;
898
- }
899
-
900
888
  if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
901
889
  return false;
902
890
  }
@@ -912,10 +900,6 @@ export const hasSwiftXCTUnwrapUsage = (source: string): boolean => {
912
900
  return false;
913
901
  }
914
902
 
915
- if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
916
- return false;
917
- }
918
-
919
903
  if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
920
904
  return false;
921
905
  }
@@ -932,10 +916,6 @@ const hasSwiftConfirmationUsage = (source: string): boolean => {
932
916
  };
933
917
 
934
918
  export const hasSwiftWaitForExpectationsUsage = (source: string): boolean => {
935
- if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
936
- return false;
937
- }
938
-
939
919
  const legacyWaitPattern = /\bwait\s*\(\s*for\s*:|\bwaitForExpectations\s*\(/;
940
920
  return collectSwiftFunctionDeclarations(source).some((declaration) => {
941
921
  if (!/\basync\b/.test(declaration.signature)) {
@@ -947,10 +927,6 @@ export const hasSwiftWaitForExpectationsUsage = (source: string): boolean => {
947
927
  };
948
928
 
949
929
  export const hasSwiftLegacyExpectationDescriptionUsage = (source: string): boolean => {
950
- if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
951
- return false;
952
- }
953
-
954
930
  return collectSwiftFunctionDeclarations(source).some((declaration) => {
955
931
  if (!/\basync\b/.test(declaration.signature)) {
956
932
  return false;
@@ -340,10 +340,7 @@ type AstNodeWithAncestors = {
340
340
  ancestors: readonly AstNode[];
341
341
  };
342
342
 
343
- const toPositiveLine = (node: unknown): number | null => {
344
- if (!isObject(node)) {
345
- return null;
346
- }
343
+ const toPositiveLine = (node: AstNode): number | null => {
347
344
  const loc = node.loc;
348
345
  if (!isObject(loc)) {
349
346
  return null;
@@ -748,18 +745,15 @@ const toSemanticMemberNode = (
748
745
  };
749
746
  };
750
747
 
751
- const toSingleLineArray = (line: number | null | undefined): readonly number[] | undefined => {
748
+ const toSingleLineArray = (line: number | null): readonly number[] | undefined => {
752
749
  return typeof line === 'number' ? [line] : undefined;
753
750
  };
754
751
 
755
752
  const dedupeSemanticNodes = (
756
- nodes: ReadonlyArray<TypeScriptSemanticNode | undefined>
753
+ nodes: ReadonlyArray<TypeScriptSemanticNode>
757
754
  ): readonly TypeScriptSemanticNode[] => {
758
755
  const unique = new Map<string, TypeScriptSemanticNode>();
759
756
  for (const node of nodes) {
760
- if (!node) {
761
- continue;
762
- }
763
757
  const key = `${node.kind}:${node.name}:${(node.lines ?? []).join(',')}`;
764
758
  if (!unique.has(key)) {
765
759
  unique.set(key, node);
@@ -3361,10 +3355,7 @@ const buildDtoBoundaryMatch = (node: unknown): TypeScriptDtoBoundaryMatch | unde
3361
3355
 
3362
3356
  const classNode = match.node;
3363
3357
  const className = methodNameFromNode(classNode.id);
3364
- const classBody =
3365
- isObject(classNode.body) && classNode.body.type === 'ClassBody' && Array.isArray(classNode.body.body)
3366
- ? classNode.body.body
3367
- : [];
3358
+ const classBody = isObject(classNode.body) && classNode.body.type === 'ClassBody' ? classNode.body.body : [];
3368
3359
  const classLine = toPositiveLine(classNode);
3369
3360
 
3370
3361
  const propertyEntries = classBody.filter((member) => {
@@ -3734,12 +3725,12 @@ const buildBackendTransactionsMatch = (
3734
3725
  return undefined;
3735
3726
  }
3736
3727
 
3737
- const ownerNode: TypeScriptSemanticNode = semanticOwnerFromAncestors(match.ancestors) ?? {
3728
+ const ownerNode = semanticOwnerFromAncestors(match.ancestors) ?? {
3738
3729
  kind: 'member',
3739
3730
  name: `${boundaryObjectName}.${boundaryName}`,
3740
3731
  lines: toSingleLineArray(callLine),
3741
3732
  };
3742
- const primaryNode: TypeScriptSemanticNode =
3733
+ const primaryNode =
3743
3734
  ownerNode.kind === 'class' || ownerNode.kind === 'member'
3744
3735
  ? ownerNode
3745
3736
  : {
@@ -4163,7 +4154,7 @@ const buildRateLimitingThrottlerMatch = (
4163
4154
  ...(calleeName === 'Throttle'
4164
4155
  ? [
4165
4156
  {
4166
- kind: 'member' as const,
4157
+ kind: 'member',
4167
4158
  name: '@nestjs/throttler',
4168
4159
  lines: toSingleLineArray(callLine),
4169
4160
  },
@@ -4172,7 +4163,7 @@ const buildRateLimitingThrottlerMatch = (
4172
4163
  ...(calleeName === 'UseGuards'
4173
4164
  ? [
4174
4165
  {
4175
- kind: 'member' as const,
4166
+ kind: 'member',
4176
4167
  name: 'ThrottlerGuard',
4177
4168
  lines: toSingleLineArray(callLine),
4178
4169
  },
@@ -4181,7 +4172,7 @@ const buildRateLimitingThrottlerMatch = (
4181
4172
  ...(calleeName === 'forRoot' || calleeName === 'forRootAsync'
4182
4173
  ? [
4183
4174
  {
4184
- kind: 'member' as const,
4175
+ kind: 'member',
4185
4176
  name: 'ThrottlerModule',
4186
4177
  lines: toSingleLineArray(callLine),
4187
4178
  },
@@ -4566,7 +4557,7 @@ const buildCallbackHellPatternMatch = (
4566
4557
  lines: toSingleLineArray(toPositiveLine(nestedCall)),
4567
4558
  }
4568
4559
  : undefined,
4569
- ]
4560
+ ].filter((entry): entry is TypeScriptSemanticNode => entry !== undefined)
4570
4561
  );
4571
4562
  const lines = sortedUniqueLines([
4572
4563
  ...(primaryNode.lines ?? []),
@@ -4854,12 +4845,7 @@ const isRuntimeApiLiteral = (node: AstNode): boolean => {
4854
4845
  };
4855
4846
 
4856
4847
  const isNeutralHardcodedNumericLiteral = (node: AstNode): boolean => {
4857
- return (
4858
- node.type === 'NumericLiteral' &&
4859
- (node.value === 0 ||
4860
- node.value === 1 ||
4861
- (typeof node.value === 'number' && node.value >= -32768 && node.value <= -32000))
4862
- );
4848
+ return node.type === 'NumericLiteral' && (node.value === 0 || node.value === 1);
4863
4849
  };
4864
4850
 
4865
4851
  const isBenignHardcodedConfigLiteral = (node: AstNode): boolean => {
@@ -5055,12 +5041,7 @@ const buildEnvDefaultFallbackPatternMatch = (
5055
5041
  if (value.operator !== '||' && value.operator !== '??') {
5056
5042
  return false;
5057
5043
  }
5058
- const leftNode = value.left;
5059
- return (
5060
- isObject(leftNode) &&
5061
- typeof memberExpressionPropertyName(leftNode) === 'string' &&
5062
- isProcessEnvBaseAccess(leftNode.object)
5063
- );
5044
+ return typeof memberExpressionPropertyName(value.left) === 'string' && isProcessEnvBaseAccess(value.left.object);
5064
5045
  }
5065
5046
 
5066
5047
  if (value.type !== 'AssignmentPattern') {
@@ -5507,7 +5488,6 @@ const buildProductionMockMatch = (
5507
5488
  return (
5508
5489
  isObject(objectNode) &&
5509
5490
  objectNode.type === 'Identifier' &&
5510
- typeof objectNode.name === 'string' &&
5511
5491
  productionMockLibraryPattern.test(objectNode.name) &&
5512
5492
  isObject(propertyNode) &&
5513
5493
  propertyNode.type === 'Identifier' &&
@@ -5664,7 +5644,7 @@ const buildExceptionFilterMatch = (
5664
5644
  return methodNameFromNode(expression) === 'Catch';
5665
5645
  });
5666
5646
 
5667
- const primaryNode: TypeScriptSemanticNode = {
5647
+ const primaryNode = {
5668
5648
  kind: 'class',
5669
5649
  name: className,
5670
5650
  lines: typeof classLine === 'number' ? [classLine] : [],
@@ -5679,7 +5659,7 @@ const buildExceptionFilterMatch = (
5679
5659
  ...(hasCatchDecorator
5680
5660
  ? [
5681
5661
  {
5682
- kind: 'member' as const,
5662
+ kind: 'member',
5683
5663
  name: '@Catch',
5684
5664
  lines: typeof classLine === 'number' ? [classLine] : [],
5685
5665
  },
@@ -5755,8 +5735,7 @@ const buildGuardMatch = (node: unknown): TypeScriptGuardMatch | undefined => {
5755
5735
  ? classOrMemberNode.lines[0]
5756
5736
  : toPositiveLine(classOrMemberNode ?? decoratorNode);
5757
5737
  const decoratorLine = toPositiveLine(decoratorNode);
5758
- const expressionArguments = Array.isArray(expression.arguments) ? expression.arguments : [];
5759
- const guardArgument = expressionArguments.find((argument) => methodNameFromNode(argument) === 'JwtAuthGuard');
5738
+ const guardArgument = expression.arguments.find((argument) => methodNameFromNode(argument) === 'JwtAuthGuard');
5760
5739
  const guardLine = toPositiveLine(guardArgument) ?? decoratorLine;
5761
5740
  const ownerName =
5762
5741
  classOrMemberNode?.kind === 'class' || classOrMemberNode?.kind === 'member'
@@ -5861,8 +5840,7 @@ const buildInterceptorMatch = (node: unknown): TypeScriptInterceptorMatch | unde
5861
5840
  ? classOrMemberNode.lines[0]
5862
5841
  : toPositiveLine(classOrMemberNode ?? decoratorNode);
5863
5842
  const decoratorLine = toPositiveLine(decoratorNode);
5864
- const expressionArguments = Array.isArray(expression.arguments) ? expression.arguments : [];
5865
- const interceptorArgument = expressionArguments.find((argument) => {
5843
+ const interceptorArgument = expression.arguments.find((argument) => {
5866
5844
  const name = interceptorNameFromNode(argument);
5867
5845
  return typeof name === 'string' && /Interceptor$/u.test(name);
5868
5846
  });
@@ -6191,7 +6169,7 @@ const buildReactClassComponentMatch = (
6191
6169
  lines: toSingleLineArray(reactImportInfo.importLines[0]),
6192
6170
  }
6193
6171
  : undefined,
6194
- ]);
6172
+ ].filter((entry): entry is TypeScriptSemanticNode => entry !== undefined));
6195
6173
 
6196
6174
  const lines = sortedUniqueLines([
6197
6175
  ...(typeof classLine === 'number' ? [classLine] : []),
@@ -6384,7 +6362,7 @@ const buildSingletonPatternMatch = (
6384
6362
  lines: toSingleLineArray(toPositiveLine(singletonFactoryMethodMember)),
6385
6363
  }
6386
6364
  : undefined,
6387
- ]
6365
+ ].filter((entry): entry is TypeScriptSemanticNode => entry !== undefined)
6388
6366
  );
6389
6367
 
6390
6368
  const lines = sortedUniqueLines([
@@ -460,7 +460,6 @@ type ASTDetectorRegistryEntry = {
460
460
  readonly code: string;
461
461
  readonly message: string;
462
462
  readonly pathCheck?: (path: string) => boolean;
463
- readonly excludePaths?: ReadonlyArray<(path: string) => boolean>;
464
463
  readonly includeTestPaths?: boolean;
465
464
  };
466
465
 
@@ -4,12 +4,6 @@ This file tracks the active deterministic framework line used in this repository
4
4
  Canonical release chronology lives in `CHANGELOG.md`.
5
5
  This file keeps only the operational highlights and rollout notes that matter while running the framework.
6
6
 
7
- ### 2026-05-07 (v6.3.170)
8
-
9
- - **npm README entrypoint:** la ficha pública de npm pasa a mostrar el README premium real, con capturas actuales de menú/auditoría, límites explícitos de hooks/CI, binarios MCP stdio y cobertura AUTO vs DECLARATIVE de skills.
10
- - **Release typecheck:** la línea publicada recupera `npm run typecheck` con fixes conservadores en detectores TypeScript y helpers lifecycle/MCP, sin cambiar semántica de detección.
11
- - **Rollout:** publicar `pumuki@6.3.170` para sustituir el README antiguo visible en `pumuki@6.3.169` sin degradar la línea de paquete ya publicada.
12
-
13
7
  ### 2026-05-05 (v6.3.150)
14
8
 
15
9
  - **RuralGo PUMUKI-INC-061:** las notificaciones de bloqueo ya recomiendan `policy reconcile --strict --apply --json` cuando la remediación real requiere converger policy-as-code.
@@ -8,15 +8,6 @@ const PACKAGE_ROOT = resolve(__dirname, '..', '..');
8
8
 
9
9
  let cachedCoreSkillsLock: SkillsLockV1 | undefined;
10
10
 
11
- const writeDebugFallback = (error: unknown): void => {
12
- if (process.env.PUMUKI_DEBUG !== '1') {
13
- return;
14
- }
15
-
16
- const message = error instanceof Error ? error.message : String(error);
17
- process.stderr.write(`[pumuki][skills-lock] compile fallback: ${message}\n`);
18
- };
19
-
20
11
  export const resolveCoreSkillsLockForPackageRoot = (
21
12
  packageRoot: string
22
13
  ): SkillsLockV1 | undefined => {
@@ -29,8 +20,7 @@ export const resolveCoreSkillsLockForPackageRoot = (
29
20
  if (compiledLock.bundles.length > 0) {
30
21
  return compiledLock;
31
22
  }
32
- } catch (error) {
33
- writeDebugFallback(error);
23
+ } catch {
34
24
  }
35
25
 
36
26
  return loadSkillsLock(packageRoot);
@@ -603,6 +603,7 @@ export const loadSkillsRuleSetForStage = (
603
603
  continue;
604
604
  }
605
605
  if (evaluationMode !== 'AUTO') {
606
+ unsupportedDetectorRuleIds.add(compiledRule.id);
606
607
  continue;
607
608
  }
608
609
  stageApplicableAutoRuleIds.add(compiledRule.id);
@@ -77,9 +77,6 @@ const shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope = (params: {
77
77
  return paths.every(isDocumentationOnlyStagedPath);
78
78
  };
79
79
 
80
- const hasStagedEvidencePath = (paths: ReadonlyArray<string>): boolean =>
81
- paths.some((path) => path === '.ai_evidence.json' || path === '.AI_EVIDENCE.json');
82
-
83
80
  const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
84
81
  const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
85
82
  const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
@@ -523,7 +520,6 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
523
520
  dependencies: StageRunnerDependencies;
524
521
  repoRoot: string;
525
522
  gateBlocked: boolean;
526
- evidenceWasStagedAtStart: boolean;
527
523
  }): boolean => {
528
524
  const evidenceAbsolutePath = join(params.repoRoot, EVIDENCE_FILE_PATH);
529
525
  if (!existsSync(evidenceAbsolutePath)) {
@@ -547,20 +543,6 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
547
543
  params.dependencies.restorePathFromHead(params.repoRoot, EVIDENCE_FILE_PATH);
548
544
  return false;
549
545
  }
550
- if (
551
- !params.gateBlocked &&
552
- !params.evidenceWasStagedAtStart &&
553
- !isTruthyEnvFlag(process.env.PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE)
554
- ) {
555
- if (!params.dependencies.isQuietMode()) {
556
- process.stderr.write(
557
- `[pumuki][evidence-sync] tracked ${EVIDENCE_FILE_PATH} restored because it was not staged before PRE_COMMIT. ` +
558
- `Force previous behavior: PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1\n`
559
- );
560
- }
561
- params.dependencies.restorePathFromHead(params.repoRoot, EVIDENCE_FILE_PATH);
562
- return false;
563
- }
564
546
  try {
565
547
  params.dependencies.stagePath(params.repoRoot, EVIDENCE_FILE_PATH);
566
548
  return false;
@@ -805,7 +787,6 @@ export async function runPreCommitStage(
805
787
  const activeDependencies = getDependencies(dependencies);
806
788
  const repoRoot = activeDependencies.resolveRepoRoot();
807
789
  const manifestSnapshot = captureManifestGuardSnapshot(repoRoot);
808
- const initiallyStagedPaths = activeDependencies.listStagedIndexPaths(repoRoot);
809
790
  activeDependencies.ensureRuntimeArtifactsIgnored(repoRoot);
810
791
  if (
811
792
  enforceGitAtomicityGate({
@@ -839,7 +820,6 @@ export async function runPreCommitStage(
839
820
  dependencies: activeDependencies,
840
821
  repoRoot,
841
822
  gateBlocked: result.exitCode !== 0,
842
- evidenceWasStagedAtStart: hasStagedEvidencePath(initiallyStagedPaths),
843
823
  })
844
824
  ) {
845
825
  return 1;
@@ -4,7 +4,7 @@ import { GitService, type IGitService } from '../git/GitService';
4
4
  import { hasAllowedExtension } from '../git/gitDiffUtils';
5
5
  import { runPlatformGate } from '../git/runPlatformGate';
6
6
  import { evaluatePlatformGateFindings } from '../git/runPlatformGateEvaluation';
7
- import { DEFAULT_FACT_FILE_EXTENSIONS } from '../git/runPlatformGateFacts';
7
+ import { DEFAULT_FACT_FILE_EXTENSIONS, type GateScope } from '../git/runPlatformGateFacts';
8
8
  import { resolvePolicyForStage, type ResolvedStagePolicy } from '../gate/stagePolicies';
9
9
 
10
10
  export type LifecycleAuditStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
@@ -23,7 +23,7 @@ export type LifecycleAuditResult = {
23
23
  command: 'pumuki audit';
24
24
  repo_root: string;
25
25
  stage: LifecycleAuditStage;
26
- scope: { kind: 'repo' };
26
+ scope: { kind: 'repo' | 'staged'; staged_matching_extensions_count: number };
27
27
  audit_mode: 'gate' | 'engine';
28
28
  gate_exit_code: number;
29
29
  files_scanned: number | null;
@@ -70,6 +70,28 @@ const countUntrackedMatchingExtensions = (
70
70
  .filter((path) => hasAllowedExtension(path, extensions)).length;
71
71
  };
72
72
 
73
+ const collectStagedMatchingExtensions = (
74
+ git: Pick<IGitService, 'resolveRepoRoot' | 'runGit'>,
75
+ extensions: ReadonlyArray<string>
76
+ ): string[] => {
77
+ const repoRoot = git.resolveRepoRoot();
78
+ return git.runGit(['diff', '--cached', '--name-only'], repoRoot)
79
+ .split('\n')
80
+ .map((line) => line.trim())
81
+ .filter((line) => line.length > 0)
82
+ .filter((path) => hasAllowedExtension(path, extensions));
83
+ };
84
+
85
+ const resolveLifecycleAuditScope = (params: {
86
+ stage: LifecycleAuditStage;
87
+ stagedMatchingExtensions: ReadonlyArray<string>;
88
+ }): GateScope => {
89
+ if (params.stage === 'PRE_WRITE' && params.stagedMatchingExtensions.length > 0) {
90
+ return { kind: 'staged' };
91
+ }
92
+ return { kind: 'repo' };
93
+ };
94
+
73
95
  const isFindingBlocking = (finding: SnapshotFinding): boolean => {
74
96
  return finding.severity === 'CRITICAL' ||
75
97
  finding.severity === 'ERROR' ||
@@ -179,13 +201,18 @@ export const runLifecycleAudit = async (params: {
179
201
  );
180
202
  const extensions = DEFAULT_FACT_FILE_EXTENSIONS;
181
203
  const untrackedMatchingExtensionsCount = countUntrackedMatchingExtensions(git, extensions);
204
+ const stagedMatchingExtensions = collectStagedMatchingExtensions(git, extensions);
205
+ const scope = resolveLifecycleAuditScope({
206
+ stage: params.stage,
207
+ stagedMatchingExtensions,
208
+ });
182
209
 
183
210
  const gateParams =
184
211
  params.auditMode === 'engine'
185
212
  ? {
186
213
  policy: resolved.policy,
187
214
  policyTrace: resolved.trace,
188
- scope: { kind: 'repo' as const },
215
+ scope,
189
216
  silent: true,
190
217
  auditMode: 'engine' as const,
191
218
  dependencies: {
@@ -204,7 +231,7 @@ export const runLifecycleAudit = async (params: {
204
231
  : {
205
232
  policy: resolved.policy,
206
233
  policyTrace: resolved.trace,
207
- scope: { kind: 'repo' as const },
234
+ scope,
208
235
  silent: true,
209
236
  auditMode: 'gate' as const,
210
237
  };
@@ -229,7 +256,10 @@ export const runLifecycleAudit = async (params: {
229
256
  command: 'pumuki audit',
230
257
  repo_root: repoRoot,
231
258
  stage: params.stage,
232
- scope: { kind: 'repo' },
259
+ scope: {
260
+ kind: scope.kind === 'staged' ? 'staged' : 'repo',
261
+ staged_matching_extensions_count: stagedMatchingExtensions.length,
262
+ },
233
263
  audit_mode: params.auditMode,
234
264
  gate_exit_code: gateExitCode,
235
265
  files_scanned: filesScanned,
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
- import type { AiGateStage } from '../gate/evaluateAiGate'
4
3
  import { getPumukiHooksStatus, resolvePumukiHooksDirectory } from './hookManager'
5
4
  import { LifecycleGitService, type ILifecycleGitService } from './gitService'
6
5
  import {
@@ -16,15 +15,6 @@ import { readLifecycleState } from './state'
16
15
 
17
16
  export const BOOTSTRAP_MANIFEST_RELATIVE_PATH = '.pumuki/bootstrap-manifest.json'
18
17
 
19
- const JSON_PRETTY_PRINT_SPACES = 2
20
-
21
- const toAiGateStage = (stage: string | undefined): AiGateStage => {
22
- if (stage === 'PRE_WRITE' || stage === 'PRE_COMMIT' || stage === 'PRE_PUSH' || stage === 'CI') {
23
- return stage
24
- }
25
- return 'PRE_WRITE'
26
- }
27
-
28
18
  type AdapterCommandContract = {
29
19
  path: string
30
20
  present: boolean
@@ -184,7 +174,7 @@ export const buildLifecycleBootstrapManifest = (params: {
184
174
  })
185
175
  const governanceNextAction = readGovernanceNextAction({
186
176
  repoRoot,
187
- stage: toAiGateStage(governanceObservation.evidence.snapshot_stage),
177
+ stage: governanceObservation.evidence.snapshot_stage ?? 'PRE_WRITE',
188
178
  governanceObservation,
189
179
  })
190
180
  const hooksDirectory = resolvePumukiHooksDirectory(repoRoot)
@@ -233,7 +223,7 @@ export const buildLifecycleBootstrapManifest = (params: {
233
223
  }
234
224
 
235
225
  const serializeManifest = (manifest: LifecycleBootstrapManifest): string =>
236
- `${JSON.stringify(manifest, null, JSON_PRETTY_PRINT_SPACES)}\n`
226
+ `${JSON.stringify(manifest, null, 2)}\n`
237
227
 
238
228
  export const writeLifecycleBootstrapManifest = (params: {
239
229
  repoRoot: string
@@ -10,6 +10,7 @@ import type { ILifecycleGitService } from './gitService';
10
10
  import { LifecycleGitService } from './gitService';
11
11
  import type { LifecyclePolicyValidationSnapshot } from './policyValidationSnapshot';
12
12
  import { writeInfo } from './cliOutputs';
13
+ import { formatTrackingActionableContext } from './trackingState';
13
14
 
14
15
  const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
15
16
  const STRICT_ENV_VALUE = 'strict';
@@ -205,8 +206,9 @@ const buildHints = (
205
206
  hints.push(`Falta el tracking canónico declarado (${tracking.canonical_path ?? 'sin resolver'}).`);
206
207
  }
207
208
  if (tracking.enforced && tracking.single_in_progress_valid === false) {
209
+ const actionableContext = formatTrackingActionableContext(tracking);
208
210
  hints.push(
209
- `El tracking canónico debe dejar exactamente una 🚧 (actual=${tracking.in_progress_count ?? 'n/a'}).`
211
+ `El tracking canónico debe dejar exactamente una 🚧 (actual=${tracking.in_progress_count ?? 'n/a'}${actionableContext ? `, ${actionableContext}` : ''}).`
210
212
  );
211
213
  }
212
214
  hints.push('SDD/OpenSpec: usa PUMUKI_EXPERIMENTAL_SDD=advisory|strict cuando el loop SDD esté activo.');
@@ -349,8 +351,9 @@ export const buildGovernanceObservationSummaryLines = (
349
351
  lines.push(`Attention: ${snapshot.attention_codes.join(', ')}`);
350
352
  }
351
353
  if (snapshot.tracking.enforced && snapshot.tracking.single_in_progress_valid === false) {
354
+ const actionableContext = formatTrackingActionableContext(snapshot.tracking);
352
355
  lines.push(
353
- `Tracking: canonical=${snapshot.tracking.canonical_path ?? 'unknown'} in_progress_count=${snapshot.tracking.in_progress_count ?? 'n/a'}`
356
+ `Tracking: canonical=${snapshot.tracking.canonical_path ?? 'unknown'} in_progress_count=${snapshot.tracking.in_progress_count ?? 'n/a'}${actionableContext ? ` ${actionableContext}` : ''}`
354
357
  );
355
358
  }
356
359
  return lines;
@@ -13,7 +13,6 @@ import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
13
13
  import { readOpenSpecManagedArtifacts, writeLifecycleState } from './state';
14
14
  import { ensureRuntimeArtifactsIgnored } from './artifacts';
15
15
  import { runLifecycleAdapterInstall } from './adapter';
16
- import { writeLifecycleBootstrapManifest } from './bootstrapManifest';
17
16
  import { runPolicyReconcile } from './policyReconcile';
18
17
  import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification';
19
18
 
@@ -21,10 +20,6 @@ export type LifecycleInstallResult = {
21
20
  repoRoot: string;
22
21
  version: string;
23
22
  changedHooks: ReadonlyArray<string>;
24
- bootstrapManifest: {
25
- path: string;
26
- changed: boolean;
27
- };
28
23
  openSpecBootstrap?: OpenSpecBootstrapResult;
29
24
  degradedDoctorBypass?: boolean;
30
25
  };
@@ -132,18 +127,10 @@ export const runLifecycleInstall = (params?: {
132
127
  });
133
128
  ensureRepoBaselineAdapter(report.repoRoot);
134
129
  materializeStrictPolicyAsCode(report.repoRoot);
135
- const bootstrapManifest = writeLifecycleBootstrapManifest({
136
- git,
137
- repoRoot: report.repoRoot,
138
- });
139
130
  return {
140
131
  repoRoot: report.repoRoot,
141
132
  version,
142
133
  changedHooks,
143
- bootstrapManifest: {
144
- path: bootstrapManifest.path,
145
- changed: bootstrapManifest.changed,
146
- },
147
134
  openSpecBootstrap: undefined,
148
135
  degradedDoctorBypass: true,
149
136
  };
@@ -192,19 +179,11 @@ export const runLifecycleInstall = (params?: {
192
179
  });
193
180
  ensureRepoBaselineAdapter(report.repoRoot);
194
181
  materializeStrictPolicyAsCode(report.repoRoot);
195
- const bootstrapManifest = writeLifecycleBootstrapManifest({
196
- git,
197
- repoRoot: report.repoRoot,
198
- });
199
182
 
200
183
  return {
201
184
  repoRoot: report.repoRoot,
202
185
  version,
203
186
  changedHooks,
204
- bootstrapManifest: {
205
- path: bootstrapManifest.path,
206
- changed: bootstrapManifest.changed,
207
- },
208
187
  openSpecBootstrap,
209
188
  };
210
189
  };
@@ -52,14 +52,10 @@ const resolvePrePushBootstrapBaseRefInRepo = (repoRoot: string): string => {
52
52
  continue;
53
53
  }
54
54
  try {
55
- const diffOutput = runGit(
55
+ const changedFiles = runGit(
56
56
  repoRoot,
57
57
  ['diff', '--name-only', '--diff-filter=ACMR', `${candidate}..HEAD`]
58
- );
59
- if (diffOutput === null) {
60
- continue;
61
- }
62
- const changedFiles = diffOutput
58
+ )
63
59
  .split('\n')
64
60
  .map((line) => line.trim())
65
61
  .filter((line) => line.length > 0).length;