@planu/cli 4.10.4 → 4.10.6

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/CHANGELOG.md +17 -1
  2. package/dist/config/version.js +1 -1
  3. package/dist/config/version.ts +1 -1
  4. package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
  5. package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
  6. package/dist/engine/evidence-index/done-drift.js +4 -1
  7. package/dist/engine/local-first/tool-classification.d.ts +6 -0
  8. package/dist/engine/local-first/tool-classification.js +119 -0
  9. package/dist/engine/project-graph/extractors/decision-store-extractor.js +1 -1
  10. package/dist/engine/project-graph/extractors/git-extractor.js +1 -1
  11. package/dist/engine/project-graph/extractors/handoff-extractor.js +1 -1
  12. package/dist/engine/project-graph/extractors/spec-extractor.js +1 -1
  13. package/dist/engine/project-graph/extractors/validation-extractor.js +1 -1
  14. package/dist/engine/project-graph/index.d.ts +2 -1
  15. package/dist/engine/project-graph/index.js +2 -1
  16. package/dist/engine/project-graph/query.d.ts +1 -2
  17. package/dist/engine/project-graph/query.js +1 -10
  18. package/dist/engine/project-graph/redaction.d.ts +4 -0
  19. package/dist/engine/project-graph/redaction.js +11 -0
  20. package/dist/engine/qa-gate.js +13 -1
  21. package/dist/engine/scope-boundaries/scope-validator.js +14 -2
  22. package/dist/engine/validator/validation-report-writer.js +39 -8
  23. package/dist/tools/design-schema-sql/migrations.js +8 -3
  24. package/dist/tools/design-schema-sql/tables.js +19 -3
  25. package/dist/tools/list-specs.js +15 -59
  26. package/dist/tools/output-compressor.d.ts +14 -1
  27. package/dist/tools/output-compressor.js +121 -2
  28. package/dist/tools/package-handoff.js +136 -12
  29. package/dist/tools/status-handler.js +6 -1
  30. package/dist/tools/token-recording.d.ts +2 -1
  31. package/dist/tools/token-recording.js +21 -2
  32. package/dist/tools/update-status/dod-gates.d.ts +2 -2
  33. package/dist/tools/update-status/dod-gates.js +8 -28
  34. package/dist/tools/update-status/index.js +7 -1
  35. package/dist/tools/update-status/qa-gate.d.ts +5 -0
  36. package/dist/tools/update-status/qa-gate.js +50 -0
  37. package/dist/tools/update-status/response-builder.js +96 -2
  38. package/dist/tools/update-status-convention-gate.js +22 -6
  39. package/dist/tools/validate-lint.js +1 -1
  40. package/dist/tools/validate.js +147 -19
  41. package/dist/transports/transport-factory.js +13 -0
  42. package/dist/types/qa-gate.d.ts +3 -0
  43. package/dist/types/tool-classification.d.ts +8 -0
  44. package/dist/types/tool-classification.js +2 -0
  45. package/package.json +11 -18
  46. package/planu-native.json +8 -29
  47. package/planu-plugin.json +7 -35
  48. package/dist/engine/context-artifacts/index.d.ts +0 -2
  49. package/dist/engine/context-artifacts/index.js +0 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.10.6] - 2026-07-07
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1111): stabilize update_status done gates and evidence state transitions
5
+ - fix(SPEC-1112): preserve validation-report lint evidence during done gate
6
+
7
+ ### Improvements
8
+ - refactor(SPEC-1110): reduce MCP token payloads with local-first tool classification
9
+
10
+
11
+ ## [4.10.5] - 2026-07-07
12
+
13
+ ### Bug Fixes
14
+ - fix(SPEC-1109): resolve audit backlog
15
+
16
+
1
17
  ## [4.10.4] - 2026-07-06
2
18
 
3
19
  ### Bug Fixes
@@ -4183,4 +4199,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4183
4199
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4184
4200
  - Multi-language i18n (EN/ES/PT) for generated specs
4185
4201
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4186
- - 10,857 tests with ≥95% coverage
4202
+ - 10,857 tests with ≥95% coverage
@@ -1,4 +1,4 @@
1
- // version.ts — reads from package.json at runtime; updated automatically by semantic-release
1
+ // version.ts — reads from package.json at runtime; updated by the local release flow
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -1,4 +1,4 @@
1
- // version.ts — reads from package.json at runtime; updated automatically by semantic-release
1
+ // version.ts — reads from package.json at runtime; updated by the local release flow
2
2
 
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
@@ -3,5 +3,5 @@ export declare function compactToolList(tools: unknown[]): Record<string, unknow
3
3
  /** Returns true when an outgoing JSON-RPC message is a tools/list result. */
4
4
  export declare function isToolsListResponse(message: unknown): boolean;
5
5
  /** Compact a tools/list JSON-RPC response message. Returns a new object. */
6
- export declare function compactToolsListMessage(message: Record<string, unknown>): Record<string, unknown>;
6
+ export declare function compactToolsListMessage<T extends Record<string, unknown>>(message: T): T;
7
7
  //# sourceMappingURL=tool-list-compactor.d.ts.map
@@ -114,9 +114,25 @@ export function checkDoneEvidenceGate(spec, criteria, artifacts) {
114
114
  !hasText(row.validationEvidence) ||
115
115
  !hasText(row.reviewerEvidence));
116
116
  if (incompleteRows.length > 0) {
117
+ const rowDetails = incompleteRows.map((row) => {
118
+ const missing = [];
119
+ if (!rowHasEvidence(row)) {
120
+ missing.push('scenario/testEvidence/contractEvidence/manualEvidence');
121
+ }
122
+ if (row.changedFiles.length === 0) {
123
+ missing.push('changedFiles');
124
+ }
125
+ if (!hasText(row.validationEvidence)) {
126
+ missing.push('validationEvidence');
127
+ }
128
+ if (!hasText(row.reviewerEvidence)) {
129
+ missing.push('reviewerEvidence');
130
+ }
131
+ return `${row.acceptanceCriterion}: missing ${missing.join(', ')}`;
132
+ });
117
133
  issues.push({
118
134
  code: 'traceability_incomplete_rows',
119
- message: 'Traceability rows must include scenario/test/contract/manual evidence, changed files, validation evidence, and reviewer evidence.',
135
+ message: `Traceability rows must include evidence, changed files, validation evidence, and reviewer evidence. Incomplete rows: ${rowDetails.join('; ')}`,
120
136
  });
121
137
  }
122
138
  }
@@ -33,9 +33,12 @@ export function checkDoneDriftContract(args) {
33
33
  .map((record) => `${entry.criterion}: ${record.value}`);
34
34
  });
35
35
  if (unapproved.length > 0) {
36
+ const groundedHint = args.groundedTechnicalPaths.length > 0
37
+ ? ` Grounded technical paths: ${args.groundedTechnicalPaths.join(', ')}.`
38
+ : ' No grounded technical paths were found in the approved spec.';
36
39
  issues.push({
37
40
  code: 'done_drift_unapproved_scope',
38
- message: `Done drift check found changed files outside grounded spec scope: ${unapproved.join('; ')}`,
41
+ message: `Done drift check found changed files outside grounded spec scope: ${unapproved.join('; ')}.${groundedHint} Add the file to approved technical references or include reviewerEvidence with an approved scope amendment.`,
39
42
  });
40
43
  }
41
44
  }
@@ -0,0 +1,6 @@
1
+ import type { ToolClassification } from '../../types/tool-classification.js';
2
+ export declare function getToolClassification(toolName: string): ToolClassification;
3
+ export declare function isLocalFirstTool(toolName: string): boolean;
4
+ export declare function requiresLlmByDefault(toolName: string): boolean;
5
+ export declare function listToolClassifications(): ToolClassification[];
6
+ //# sourceMappingURL=tool-classification.d.ts.map
@@ -0,0 +1,119 @@
1
+ const TOOL_CLASSIFICATIONS = new Map([
2
+ [
3
+ 'create_spec',
4
+ {
5
+ toolName: 'create_spec',
6
+ executionClass: 'llm-required',
7
+ tokenPolicy: 'llm-product',
8
+ reason: 'Generates the primary spec artifact and needs language synthesis.',
9
+ },
10
+ ],
11
+ [
12
+ 'validate',
13
+ {
14
+ toolName: 'validate',
15
+ executionClass: 'local',
16
+ tokenPolicy: 'summarize-first',
17
+ reason: 'Compares repository state and spec artifacts with deterministic validators.',
18
+ },
19
+ ],
20
+ [
21
+ 'audit',
22
+ {
23
+ toolName: 'audit',
24
+ executionClass: 'local',
25
+ tokenPolicy: 'summarize-first',
26
+ reason: 'Runs deterministic checks and returns issue summaries.',
27
+ },
28
+ ],
29
+ [
30
+ 'package_handoff',
31
+ {
32
+ toolName: 'package_handoff',
33
+ executionClass: 'local',
34
+ tokenPolicy: 'summarize-first',
35
+ reason: 'Packages existing spec and project context into an artifact reference.',
36
+ },
37
+ ],
38
+ [
39
+ 'list_specs',
40
+ {
41
+ toolName: 'list_specs',
42
+ executionClass: 'local',
43
+ tokenPolicy: 'summarize-first',
44
+ reason: 'Lists indexed spec metadata and must stay read-only.',
45
+ },
46
+ ],
47
+ [
48
+ 'summarize_spec',
49
+ {
50
+ toolName: 'summarize_spec',
51
+ executionClass: 'local',
52
+ tokenPolicy: 'summarize-first',
53
+ reason: 'Extracts bounded decision fields from an existing spec.',
54
+ },
55
+ ],
56
+ [
57
+ 'generate_tests',
58
+ {
59
+ toolName: 'generate_tests',
60
+ executionClass: 'hybrid',
61
+ tokenPolicy: 'summarize-first',
62
+ reason: 'Can generate deterministic scaffolds locally; novel test design may need LLM input.',
63
+ },
64
+ ],
65
+ [
66
+ 'recommend_model',
67
+ {
68
+ toolName: 'recommend_model',
69
+ executionClass: 'local',
70
+ tokenPolicy: 'no-llm',
71
+ reason: 'Uses local heuristics and project metadata for routing advice.',
72
+ },
73
+ ],
74
+ [
75
+ 'multi_agent_review',
76
+ {
77
+ toolName: 'multi_agent_review',
78
+ executionClass: 'hybrid',
79
+ tokenPolicy: 'summarize-first',
80
+ reason: 'Local orchestration and evidence collection precede any LLM review synthesis.',
81
+ },
82
+ ],
83
+ [
84
+ 'audit_specs_drift',
85
+ {
86
+ toolName: 'audit_specs_drift',
87
+ executionClass: 'local',
88
+ tokenPolicy: 'summarize-first',
89
+ reason: 'Diffs spec and implementation artifacts locally.',
90
+ },
91
+ ],
92
+ [
93
+ 'orchestrate_runtime',
94
+ {
95
+ toolName: 'orchestrate_runtime',
96
+ executionClass: 'local',
97
+ tokenPolicy: 'no-llm',
98
+ reason: 'Selects runtime behavior from local state and explicit policy.',
99
+ },
100
+ ],
101
+ ]);
102
+ export function getToolClassification(toolName) {
103
+ return (TOOL_CLASSIFICATIONS.get(toolName) ?? {
104
+ toolName,
105
+ executionClass: 'hybrid',
106
+ tokenPolicy: 'summarize-first',
107
+ reason: 'Unregistered tools default to local preprocessing with bounded summaries.',
108
+ });
109
+ }
110
+ export function isLocalFirstTool(toolName) {
111
+ return getToolClassification(toolName).executionClass !== 'llm-required';
112
+ }
113
+ export function requiresLlmByDefault(toolName) {
114
+ return getToolClassification(toolName).executionClass === 'llm-required';
115
+ }
116
+ export function listToolClassifications() {
117
+ return [...TOOL_CLASSIFICATIONS.values()].sort((a, b) => a.toolName.localeCompare(b.toolName));
118
+ }
119
+ //# sourceMappingURL=tool-classification.js.map
@@ -1,6 +1,6 @@
1
1
  import { safeJsonParse } from '../../../storage/base-store.js';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  function isDecision(value) {
5
5
  return (value !== null &&
6
6
  typeof value === 'object' &&
@@ -1,6 +1,6 @@
1
1
  import { promisify } from 'node:util';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  export async function extractGitGraph(args) {
5
5
  try {
6
6
  const childProcess = await import('node:child_process');
@@ -1,5 +1,5 @@
1
1
  import { hashText } from '../cache.js';
2
- import { redactGraphText } from '../query.js';
2
+ import { redactGraphText } from '../redaction.js';
3
3
  function specIdFromPath(path) {
4
4
  return path.split('/').find((part) => /^SPEC-\d+$/i.test(part));
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import YAML from 'yaml';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  function frontmatter(content) {
5
5
  if (!content.startsWith('---')) {
6
6
  return {};
@@ -1,5 +1,5 @@
1
1
  import { hashText } from '../cache.js';
2
- import { redactGraphText } from '../query.js';
2
+ import { redactGraphText } from '../redaction.js';
3
3
  function specIdFromPath(path) {
4
4
  return path.split('/').find((part) => /^SPEC-\d+$/i.test(part));
5
5
  }
@@ -1,4 +1,5 @@
1
1
  export { buildProjectKnowledgeGraph } from './builder.js';
2
- export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
3
+ export { redactGraphText } from './redaction.js';
3
4
  export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  export { buildProjectKnowledgeGraph } from './builder.js';
2
- export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
3
+ export { redactGraphText } from './redaction.js';
3
4
  export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,4 @@
1
- import type { ProjectGraphPolicy, ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
2
- export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
1
+ import type { ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
3
2
  export declare function queryProjectGraphSlice(input: ProjectGraphQueryInput): Promise<ProjectGraphSlice | null>;
4
3
  export declare function formatProjectGraphContext(slice: ProjectGraphSlice): Promise<string>;
5
4
  export declare function getProjectGraphFreshnessHint(args: {
@@ -1,16 +1,7 @@
1
1
  import { hashProjectPath } from '../../storage/base-store.js';
2
2
  import { buildProjectKnowledgeGraph } from './builder.js';
3
3
  import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph } from './cache.js';
4
- function escapeInert(text) {
5
- return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
- }
7
- export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
8
- const truncated = text.slice(0, maxChars);
9
- return policy.redaction.redactPatterns.reduce((current, pattern) => {
10
- const re = new RegExp(pattern, 'gi');
11
- return current.replace(re, '[redacted]');
12
- }, truncated);
13
- }
4
+ import { escapeInert, redactGraphText } from './redaction.js';
14
5
  function sanitizeLabel(label, policy) {
15
6
  return escapeInert(redactGraphText(label, policy));
16
7
  }
@@ -0,0 +1,4 @@
1
+ import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
2
+ export declare function escapeInert(text: string): string;
3
+ export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
4
+ //# sourceMappingURL=redaction.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function escapeInert(text) {
2
+ return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
3
+ }
4
+ export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
5
+ const truncated = text.slice(0, maxChars);
6
+ return policy.redaction.redactPatterns.reduce((current, pattern) => {
7
+ const re = new RegExp(pattern, 'gi');
8
+ return current.replace(re, '[redacted]');
9
+ }, truncated);
10
+ }
11
+ //# sourceMappingURL=redaction.js.map
@@ -20,7 +20,19 @@ function runCheck(name, command, args, cwd) {
20
20
  const durationMs = Date.now() - start;
21
21
  const passed = result.status === 0 && !result.error;
22
22
  const output = [result.stdout, result.stderr].join('\n').trim().slice(0, 2000);
23
- return { name, passed, output, durationMs };
23
+ const errorCode = result.error && 'code' in result.error
24
+ ? String(result.error.code)
25
+ : undefined;
26
+ const timedOut = errorCode === 'ETIMEDOUT' || result.signal === 'SIGTERM';
27
+ return {
28
+ name,
29
+ passed,
30
+ output,
31
+ durationMs,
32
+ command: [command, ...args].join(' '),
33
+ timedOut,
34
+ errorMessage: result.error?.message,
35
+ };
24
36
  }
25
37
  export function runQaGate(spec, projectPath) {
26
38
  const coverageThreshold = extractCoverageThreshold(spec);
@@ -11,11 +11,23 @@ function normalize(text) {
11
11
  /**
12
12
  * Extract keywords from an out-of-scope item phrase (>3 chars).
13
13
  */
14
- const STOP_WORDS = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'will']);
14
+ const STOP_WORDS = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'will', 'every']);
15
+ const GENERIC_SCOPE_TOKENS = new Set([
16
+ 'code',
17
+ 'file',
18
+ 'files',
19
+ 'module',
20
+ 'modules',
21
+ 'mcp',
22
+ 'response',
23
+ 'responses',
24
+ 'tool',
25
+ 'tools',
26
+ ]);
15
27
  function keywords(phrase) {
16
28
  return normalize(phrase)
17
29
  .split(' ')
18
- .filter((w) => w.length > 3 && !STOP_WORDS.has(w));
30
+ .filter((w) => w.length > 2 && !STOP_WORDS.has(w) && !GENERIC_SCOPE_TOKENS.has(w));
19
31
  }
20
32
  /**
21
33
  * Determine whether a file path matches an out-of-scope item.
@@ -1,15 +1,16 @@
1
- import { appendArtifact } from '../handoff-artifacts/io.js';
1
+ import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
2
2
  import { runSpecCompliance, } from './spec-compliance-runner.js';
3
3
  export async function writeImplementationReviewReport(input) {
4
4
  const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
5
5
  const gates = buildGates({
6
6
  score: input.score,
7
- lintPassed: input.lintPassed ?? true,
7
+ lintPassed: input.lintPassed,
8
8
  conventionRegression: input.conventionRegression ?? false,
9
9
  specCompliance,
10
10
  minimalityReport: input.minimalityReport,
11
11
  });
12
- const passed = gates.every((gate) => gate.passed);
12
+ const mergedGates = await preserveUnobservedGates(input, gates);
13
+ const passed = mergedGates.every((gate) => gate.passed);
13
14
  const reviewer = {
14
15
  kind: 'implementation-review-agent',
15
16
  agent: 'planu-implementation-reviewer',
@@ -20,7 +21,7 @@ export async function writeImplementationReviewReport(input) {
20
21
  schema_version: '1.0.0',
21
22
  specId: input.specId,
22
23
  passed,
23
- gates,
24
+ gates: mergedGates,
24
25
  reviewer,
25
26
  specCompliance: reportCompliance,
26
27
  minimalityReport: input.minimalityReport,
@@ -40,7 +41,7 @@ export async function writeImplementationReviewReport(input) {
40
41
  sha: artifact.sha,
41
42
  reviewer,
42
43
  passed,
43
- gates,
44
+ gates: mergedGates,
44
45
  specCompliance: reportCompliance,
45
46
  };
46
47
  }
@@ -50,11 +51,37 @@ export async function writeImplementationReviewReport(input) {
50
51
  error: err instanceof Error ? err.message : String(err),
51
52
  reviewer,
52
53
  passed,
53
- gates,
54
+ gates: mergedGates,
54
55
  specCompliance: reportCompliance,
55
56
  };
56
57
  }
57
58
  }
59
+ async function preserveUnobservedGates(input, nextGates) {
60
+ const currentGateNames = new Set(nextGates.map((gate) => gate.name));
61
+ const explicitlyObserved = new Set([
62
+ ...(input.lintPassed !== undefined ? ['lint'] : []),
63
+ ...(input.conventionRegression !== undefined ? ['convention-regression'] : []),
64
+ ...(input.minimalityReport !== undefined ? ['minimal-implementation'] : []),
65
+ ]);
66
+ const existing = await readArtifact({
67
+ projectId: input.projectId,
68
+ specId: input.specId,
69
+ kind: 'validation-report',
70
+ }).catch(() => null);
71
+ if (!existing?.ok) {
72
+ return nextGates;
73
+ }
74
+ const preserved = existing.payload.gates.filter((gate) => currentGateNames.has(gate.name) &&
75
+ !explicitlyObserved.has(gate.name) &&
76
+ (gate.name === 'lint' ||
77
+ gate.name === 'convention-regression' ||
78
+ gate.name === 'minimal-implementation'));
79
+ if (preserved.length === 0) {
80
+ return nextGates;
81
+ }
82
+ const preservedByName = new Map(preserved.map((gate) => [gate.name, gate]));
83
+ return nextGates.map((gate) => preservedByName.get(gate.name) ?? gate);
84
+ }
58
85
  function buildGates(args) {
59
86
  return [
60
87
  {
@@ -66,8 +93,12 @@ function buildGates(args) {
66
93
  },
67
94
  {
68
95
  name: 'lint',
69
- passed: args.lintPassed,
70
- reason: args.lintPassed ? undefined : 'Configured lint command failed.',
96
+ passed: args.lintPassed === true,
97
+ reason: args.lintPassed === true
98
+ ? undefined
99
+ : args.lintPassed === false
100
+ ? 'Configured lint command failed.'
101
+ : 'Lint result was not provided; report cannot claim lint passed.',
71
102
  },
72
103
  {
73
104
  name: 'convention-regression',
@@ -63,11 +63,16 @@ export function generateMigrations(tables, indexes, rlsPolicies, engine) {
63
63
  }
64
64
  for (const policy of rlsPolicies) {
65
65
  const rolesClause = policy.roles?.length ? ` TO ${policy.roles.join(', ')}` : '';
66
- const usingClause = `USING (${policy.using})`;
67
- const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : '';
66
+ const usingClause = policy.operation === 'INSERT' ? '' : ` USING (${policy.using})`;
67
+ const withCheckClause = policy.withCheck || policy.operation === 'INSERT'
68
+ ? ` WITH CHECK (${policy.withCheck ?? policy.using})`
69
+ : '';
70
+ const policyClauses = [rolesClause, usingClause, withCheckClause]
71
+ .filter((clause) => clause.length > 0)
72
+ .join('');
68
73
  migrations.push({
69
74
  order: order++,
70
- sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${rolesClause} ${usingClause}${withCheckClause};`,
75
+ sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${policyClauses};`,
71
76
  reversible: true,
72
77
  reverseSql: `DROP POLICY IF EXISTS ${policy.name} ON ${policy.table};`,
73
78
  dataLoss: false,
@@ -210,12 +210,28 @@ export function generateRLSPolicies(tables) {
210
210
  }
211
211
  else {
212
212
  policies.push({
213
- name: `${table.name}_all_authenticated`,
213
+ name: `${table.name}_insert_authenticated`,
214
214
  table: table.name,
215
- operation: 'ALL',
215
+ operation: 'INSERT',
216
+ roles: ['authenticated'],
217
+ using: 'true',
218
+ withCheck: 'true',
219
+ justification: 'Authenticated users can insert records',
220
+ }, {
221
+ name: `${table.name}_update_authenticated`,
222
+ table: table.name,
223
+ operation: 'UPDATE',
224
+ roles: ['authenticated'],
225
+ using: 'true',
226
+ withCheck: 'true',
227
+ justification: 'Authenticated users can update records',
228
+ }, {
229
+ name: `${table.name}_delete_authenticated`,
230
+ table: table.name,
231
+ operation: 'DELETE',
216
232
  roles: ['authenticated'],
217
233
  using: 'true',
218
- justification: 'All CRUD operations require authentication',
234
+ justification: 'Authenticated users can delete records',
219
235
  });
220
236
  }
221
237
  }
@@ -4,16 +4,15 @@ import { buildListSpecsSummary } from '../engine/human-summary.js';
4
4
  import { dirname } from 'node:path';
5
5
  import { checkBundledVersionGap } from '../engine/version-detector/bundled-version-checker.js';
6
6
  import { readLastModifiedTimestamp, formatRelativeDate } from '../engine/spec-changelog/index.js';
7
- import { discoverAndFlattenSpecs, filterUnprefixedSpecs, importFilesystemSpecs, migrateSpecFolderNames, reconcileSpecPaths, scanForAmbiguousCriteria, } from '../engine/spec-migrator.js';
7
+ import { scanForAmbiguousCriteria } from '../engine/spec-migrator.js';
8
8
  import { detectDrift, formatDriftMessage } from '../engine/spec-migrator/drift-detector.js';
9
9
  import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
10
- import { withAudit } from '../engine/autopilot/audit-logger.js';
11
10
  import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
12
- import { refreshProjectStatus } from '../engine/autopilot/state-updater.js';
13
11
  import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
14
12
  import { isNativeActive, fastScanSpecs } from '../engine/core-bridge.js';
15
13
  /** Track which projects have been auto-discovered this session (once per project). */
16
14
  const discoveredProjects = new Set();
15
+ const MAX_STRUCTURED_SPECS = 50;
17
16
  /** Build markdown table rows for summary mode */
18
17
  function buildSummaryTable(specs) {
19
18
  const header = '| ID | Title | Status | Type | Tags |';
@@ -21,48 +20,11 @@ function buildSummaryTable(specs) {
21
20
  const rows = specs.map((s) => `| ${s.id} | ${s.title} | ${s.status} | ${s.type} | ${s.tags.join(', ')} |`);
22
21
  return [header, divider, ...rows].join('\n');
23
22
  }
24
- async function autoDiscoverProject(projectId, projectPath, collector) {
23
+ async function collectReadOnlyProjectSignals(projectId, projectPath, collector) {
25
24
  if (discoveredProjects.has(projectId)) {
26
25
  return;
27
26
  }
28
27
  discoveredProjects.add(projectId);
29
- try {
30
- await withAudit(projectPath, 'list_specs', 'discoverAndFlattenSpecs', () => discoverAndFlattenSpecs(projectPath));
31
- }
32
- catch {
33
- /* best-effort — don't fail list_specs */
34
- }
35
- try {
36
- await withAudit(projectPath, 'list_specs', 'importFilesystemSpecs', () => importFilesystemSpecs(projectPath, {
37
- listSpecs: specStore.listSpecs,
38
- createSpec: specStore.createSpec,
39
- }, projectId), (imported) => ({ imported: Array.isArray(imported) ? imported.length : 0 }));
40
- }
41
- catch {
42
- /* best-effort */
43
- }
44
- try {
45
- await withAudit(projectPath, 'list_specs', 'reconcileSpecPaths', () => reconcileSpecPaths(projectPath, {
46
- listSpecs: specStore.listSpecs,
47
- updateSpec: specStore.updateSpec,
48
- }, projectId));
49
- }
50
- catch {
51
- /* best-effort */
52
- }
53
- try {
54
- const allStoredSpecs = await specStore.listSpecs(projectId);
55
- const unprefixed = filterUnprefixedSpecs(allStoredSpecs);
56
- if (unprefixed.length > 0) {
57
- await withAudit(projectPath, 'list_specs', 'migrateSpecFolderNames', () => migrateSpecFolderNames(projectPath, unprefixed, {
58
- listSpecs: specStore.listSpecs,
59
- updateSpec: specStore.updateSpec,
60
- }), (migrated) => ({ count: Array.isArray(migrated) ? migrated.length : 0 }));
61
- }
62
- }
63
- catch {
64
- /* best-effort */
65
- }
66
28
  // SPEC-1017: list_specs enforces the strict planu/ policy in check mode.
67
29
  // Mutating cleanup is handled by init/update/validate/housekeeping; list_specs
68
30
  // stays read-mostly and surfaces exact offenders if any remain.
@@ -88,21 +50,6 @@ async function autoDiscoverProject(projectId, projectPath, collector) {
88
50
  catch {
89
51
  /* best-effort — never block list_specs */
90
52
  }
91
- try {
92
- const { configureGitignoreForPlanu } = await import('./init-project/git-setup.js');
93
- await withAudit(projectPath, 'list_specs', 'configureGitignoreForPlanu', () => configureGitignoreForPlanu(projectPath));
94
- collector.pushOk('gitignore', 'Updated .gitignore with planu/ rules');
95
- }
96
- catch {
97
- /* best-effort */
98
- }
99
- // SPEC-493: Silently repair status.json totalSpecs count from actual store data
100
- try {
101
- await withAudit(projectPath, 'list_specs', 'refreshProjectStatus', () => refreshProjectStatus(projectPath, projectId));
102
- }
103
- catch {
104
- /* best-effort */
105
- }
106
53
  }
107
54
  export async function handleListSpecs(params) {
108
55
  // SPEC-584: list_specs is read-mostly and its migration-issue questions are opt-in.
@@ -152,9 +99,10 @@ export async function handleListSpecs(params) {
152
99
  if (isNativeActive() && knowledge.projectPath) {
153
100
  nativeBriefs = fastScanSpecs(knowledge.projectPath);
154
101
  }
155
- // Auto-discover, flatten, rename, and fix specs (once per session per project)
102
+ // Read-only project signals only. Mutating discovery/migration/repair belongs
103
+ // to init, validate, housekeeping, or explicit migration tools.
156
104
  if (knowledge.projectPath) {
157
- await autoDiscoverProject(projectId, knowledge.projectPath, collector);
105
+ await collectReadOnlyProjectSignals(projectId, knowledge.projectPath, collector);
158
106
  }
159
107
  // Get all specs
160
108
  let specs = await specStore.listSpecs(projectId);
@@ -247,6 +195,8 @@ export async function handleListSpecs(params) {
247
195
  updatedAt: s.updatedAt,
248
196
  lastModified: lastModifiedMap.get(s.id) ?? '\u2014',
249
197
  }));
198
+ const structuredSpecs = specsData.slice(0, MAX_STRUCTURED_SPECS);
199
+ const specsOmitted = Math.max(0, specsData.length - structuredSpecs.length);
250
200
  // Build text content: markdown table in summary mode, JSON in full mode
251
201
  let textContent;
252
202
  if (detail === 'summary') {
@@ -262,7 +212,13 @@ export async function handleListSpecs(params) {
262
212
  },
263
213
  count: specs.length,
264
214
  totalCount: allSpecs.length,
265
- specs: specsData,
215
+ specs: structuredSpecs,
216
+ pagination: {
217
+ limit: MAX_STRUCTURED_SPECS,
218
+ returned: structuredSpecs.length,
219
+ total: specsData.length,
220
+ omitted: specsOmitted,
221
+ },
266
222
  summary: {
267
223
  byStatus: statusCounts,
268
224
  byType: typeCounts,