prjct-cli 1.9.0 → 1.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,119 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.11.0] - 2026-02-09
4
+
5
+ ### Features
6
+
7
+ - implement sealable analysis with commit-hash signature (PRJ-263) (#153)
8
+
9
+
10
+ ## [1.11.0] - 2026-02-08
11
+
12
+ ### Features
13
+ - **Sealable Analysis**: 3-state lifecycle (draft/verified/sealed) with SHA-256 commit-hash signatures (PRJ-263)
14
+ - **Dual Storage**: Re-sync creates drafts without destroying sealed analysis — only sealed feeds task context
15
+ - **Staleness Detection**: Warns when HEAD moves past the sealed commit hash
16
+ - **Seal & Verify Commands**: `prjct seal` locks draft analysis, `prjct verify` checks integrity
17
+
18
+ ### Implementation Details
19
+ New `analysis-storage.ts` extends StorageManager with dual storage (draft + sealed). Analysis schema rewritten as Zod schemas with runtime validation. Sync service writes drafts in parallel with existing writes. Canonical JSON representation ensures deterministic SHA-256 signatures.
20
+
21
+ Key changes:
22
+ - `core/schemas/analysis.ts` — Full rewrite: plain interfaces → Zod schemas with `AnalysisStatusSchema`, `AnalysisItemSchema`
23
+ - `core/storage/analysis-storage.ts` — New: dual storage, sealing, verification, staleness detection
24
+ - `core/services/sync-service.ts` — Added `saveDraftAnalysis()` to parallel writes
25
+ - `core/commands/analysis.ts` — Added `seal()` and `verify()` command methods
26
+ - `core/commands/register.ts`, `core/index.ts` — Registered new commands
27
+
28
+ ### Test Plan
29
+
30
+ #### For QA
31
+ 1. Run `prjct sync` — verify draft analysis is created in storage
32
+ 2. Run `prjct seal` — verify analysis is locked with SHA-256 signature
33
+ 3. Run `prjct verify` — verify signature matches
34
+ 4. Run `prjct sync` again — verify sealed analysis is preserved, new draft created
35
+ 5. Make a commit, run `prjct status` — verify staleness detection warns about diverged commits
36
+
37
+ #### For Users
38
+ - **What changed:** Analysis results can now be locked (sealed) so re-syncing doesn't overwrite verified context
39
+ - **How to use:** Run `prjct seal` after reviewing sync results, `prjct verify` to check integrity
40
+ - **Breaking changes:** None — old analysis files parse with `status: 'draft'` default
41
+
42
+ ## [1.10.0] - 2026-02-08
43
+
44
+ ### Features
45
+
46
+ - redesign prompt assembly with correct section ordering + anti-hallucination (PRJ-301) (#152)
47
+ - add coordinated global token budget (PRJ-266) (#151)
48
+
49
+
50
+ ## [1.12.0] - 2026-02-07
51
+
52
+ ### Features
53
+ - **Prompt Assembly Redesign**: Correct section ordering based on research of 25+ system prompts (PRJ-301)
54
+ - **Environment Block**: Structured `<env>` block with project, git, platform, runtime, and model metadata
55
+ - **Anti-Hallucination Block**: Explicit availability/unavailability grounding injected BEFORE task context
56
+ - **Token Efficiency Directive**: Conciseness rules appended to every prompt
57
+
58
+ ### Implementation Details
59
+ Redesigned `prompt-builder.ts` section ordering to follow research-backed pattern:
60
+ Identity → Environment → Ground Truth → Capabilities → Constraints → Task Context → Task → Output Schema → Efficiency
61
+
62
+ Key changes:
63
+ - New `environment-block.ts`: Generates `<env>` XML block with auto-detected runtime, platform, and normalized names
64
+ - New `anti-hallucination.ts`: Generates constraints block from sealed analysis (available tech, absent domains, grounding rules)
65
+ - Moved template content (task instructions) to section 7 — LLM knows identity, env, and rules before reading task
66
+ - Anti-hallucination block placed at section 5 (before task context), replacing old `RULES (CRITICAL)` at the end
67
+ - Added `buildEfficiencyDirective()` with conciseness rules (max 4 lines, no preamble/postamble)
68
+ - Exported `PROMPT_SECTION_ORDER` constant and `SectionPriority` type for budget trimming
69
+ - Kept `buildCriticalRules()` as fallback when project context unavailable
70
+
71
+ ### Learnings
72
+ - Zod `.default()` only applies during `.parse()` — raw object construction skips defaults, use `??` fallback
73
+ - Renaming prompt section headers breaks existing test assertions — always update test matchers
74
+ - Template position matters: placing task instructions after constraints improves LLM grounding
75
+
76
+ ### Test Plan
77
+
78
+ #### For QA
79
+ 1. Run `bun test` — all 719 tests pass (0 failures)
80
+ 2. Run `bun run build` — build succeeds
81
+ 3. Verify `<env>` block appears in generated prompts before constraints
82
+ 4. Verify `CONSTRAINTS (Read Before Acting)` appears before template content
83
+ 5. Verify `OUTPUT RULES` section appears at end of prompt
84
+ 6. Check `AVAILABLE` and `NOT PRESENT` lists reflect project tech stack
85
+ 7. Run `prjct sync` — prompt assembly still works end-to-end
86
+
87
+ #### For Users
88
+ Prompts sent to AI models are now structured with research-backed section ordering, reducing hallucinations and improving response conciseness. No user action required — improvements are automatic.
89
+
90
+ ## [1.11.0] - 2026-02-07
91
+
92
+ ### Features
93
+ - **Token Budget Coordinator**: Centralized token budget management across all context-loading components (PRJ-266)
94
+
95
+ ### Implementation Details
96
+ Created `TokenBudgetCoordinator` class that manages the global token budget based on model context windows. Key features:
97
+ - Model context window registry (Claude 200K, Gemini 1M) with automatic budget calculation
98
+ - Input/output budget split: 65% input, 35% reserved for output
99
+ - Priority-based allocation: state (P1) > injection context (P2) > file content (P3)
100
+ - Request/record API for usage tracking with overflow detection
101
+ - Integrated into `injection-validator.ts`, `prompt-builder.ts`, and `context-selector.ts`
102
+ - Backward compatible: falls back to existing defaults when no coordinator is set
103
+
104
+ ### Test Plan
105
+
106
+ #### For QA
107
+ 1. Create coordinator with `'sonnet'` → input budget = 130K, output reserve = 70K
108
+ 2. Create with `'2.5-pro'` (Gemini) → input budget = 650K (5x Claude)
109
+ 3. Request tokens up to allocation limit → verify grants are capped
110
+ 4. Exhaust a category budget → verify subsequent requests return 0
111
+ 5. Verify `budgetsFromCoordinator()` uses coordinator's injection allocation
112
+ 6. Run full test suite → all 705 tests pass
113
+
114
+ #### For Users
115
+ Token budgets are now centrally coordinated based on the model's context window. Larger models get proportionally larger budgets automatically. No breaking changes.
116
+
3
117
  ## [1.9.0] - 2026-02-07
4
118
 
5
119
  ### Features
@@ -11,7 +125,6 @@
11
125
 
12
126
  - replace keyword domain detection with LLM semantic classification (PRJ-299) (#148)
13
127
 
14
-
15
128
  ## [1.10.0] - 2026-02-07
16
129
 
17
130
  ### Features
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Prompt Assembly Tests (PRJ-301)
3
+ *
4
+ * Tests for the redesigned prompt assembly:
5
+ * - Section ordering (Identity → Env → Ground Truth → ... → Efficiency)
6
+ * - Environment block generation
7
+ * - Anti-hallucination block generation
8
+ * - Token efficiency directive
9
+ * - Budget trimming with priorities
10
+ */
11
+
12
+ import { beforeEach, describe, expect, it } from 'bun:test'
13
+ import {
14
+ buildAntiHallucinationBlock,
15
+ type ProjectGroundTruth,
16
+ } from '../../agentic/anti-hallucination'
17
+ import { buildEnvironmentBlock, type EnvironmentBlockInput } from '../../agentic/environment-block'
18
+ import promptBuilder, { PROMPT_SECTION_ORDER } from '../../agentic/prompt-builder'
19
+
20
+ // =============================================================================
21
+ // Environment Block
22
+ // =============================================================================
23
+
24
+ describe('Environment Block (PRJ-301)', () => {
25
+ it('should generate <env> block with all fields', () => {
26
+ const input: EnvironmentBlockInput = {
27
+ projectName: 'my-app',
28
+ projectPath: '/home/user/my-app',
29
+ isGitRepo: true,
30
+ gitBranch: 'feature/login',
31
+ platform: 'darwin',
32
+ runtime: 'bun',
33
+ date: '2026-02-07',
34
+ model: 'opus',
35
+ provider: 'claude',
36
+ }
37
+
38
+ const block = buildEnvironmentBlock(input)
39
+
40
+ expect(block).toContain('<env>')
41
+ expect(block).toContain('</env>')
42
+ expect(block).toContain('project: my-app')
43
+ expect(block).toContain('path: /home/user/my-app')
44
+ expect(block).toContain('git: true')
45
+ expect(block).toContain('branch: feature/login')
46
+ expect(block).toContain('platform: macOS')
47
+ expect(block).toContain('runtime: bun')
48
+ expect(block).toContain('date: 2026-02-07')
49
+ expect(block).toContain('model: opus')
50
+ expect(block).toContain('provider: claude')
51
+ })
52
+
53
+ it('should omit undefined fields', () => {
54
+ const input: EnvironmentBlockInput = {
55
+ projectName: 'my-app',
56
+ projectPath: '/test',
57
+ }
58
+
59
+ const block = buildEnvironmentBlock(input)
60
+
61
+ expect(block).toContain('project: my-app')
62
+ expect(block).toContain('path: /test')
63
+ expect(block).not.toContain('model:')
64
+ expect(block).not.toContain('provider:')
65
+ expect(block).not.toContain('branch:')
66
+ })
67
+
68
+ it('should normalize platform names', () => {
69
+ expect(
70
+ buildEnvironmentBlock({ projectName: 'x', projectPath: '/x', platform: 'darwin' })
71
+ ).toContain('platform: macOS')
72
+ expect(
73
+ buildEnvironmentBlock({ projectName: 'x', projectPath: '/x', platform: 'linux' })
74
+ ).toContain('platform: Linux')
75
+ expect(
76
+ buildEnvironmentBlock({ projectName: 'x', projectPath: '/x', platform: 'win32' })
77
+ ).toContain('platform: Windows')
78
+ })
79
+
80
+ it('should auto-detect runtime and date when not provided', () => {
81
+ const block = buildEnvironmentBlock({ projectName: 'x', projectPath: '/x' })
82
+
83
+ // Should have a runtime (bun or node)
84
+ expect(block).toMatch(/runtime: (bun|node)/)
85
+ // Should have a date in YYYY-MM-DD format
86
+ expect(block).toMatch(/date: \d{4}-\d{2}-\d{2}/)
87
+ })
88
+ })
89
+
90
+ // =============================================================================
91
+ // Anti-Hallucination Block
92
+ // =============================================================================
93
+
94
+ describe('Anti-Hallucination Block (PRJ-301)', () => {
95
+ it('should generate constraints block with availability', () => {
96
+ const truth: ProjectGroundTruth = {
97
+ projectPath: '/home/user/my-app',
98
+ language: 'TypeScript',
99
+ framework: 'Hono',
100
+ techStack: ['Hono', 'Zod', 'Vitest'],
101
+ domains: {
102
+ hasFrontend: false,
103
+ hasBackend: true,
104
+ hasDatabase: false,
105
+ hasTesting: true,
106
+ hasDocker: false,
107
+ },
108
+ fileCount: 292,
109
+ availableAgents: ['backend', 'testing'],
110
+ }
111
+
112
+ const block = buildAntiHallucinationBlock(truth)
113
+
114
+ // Section header
115
+ expect(block).toContain('CONSTRAINTS (Read Before Acting)')
116
+
117
+ // Availability
118
+ expect(block).toContain('AVAILABLE in this project: TypeScript, Hono, Zod, Vitest')
119
+
120
+ // Unavailability (no frontend, no database, no docker)
121
+ expect(block).toContain('NOT PRESENT:')
122
+ expect(block).toContain('Frontend (UI/components)')
123
+ expect(block).toContain('Database (SQL/ORM)')
124
+ expect(block).toContain('Docker/containers')
125
+
126
+ // Should NOT list present domains as absent
127
+ expect(block).not.toContain('NOT PRESENT: Backend')
128
+ expect(block).not.toContain('NOT PRESENT: Testing')
129
+
130
+ // Agents
131
+ expect(block).toContain('AGENTS: backend, testing')
132
+
133
+ // Grounding rules
134
+ expect(block).toContain('SCOPE: Only files in `/home/user/my-app` are accessible.')
135
+ expect(block).toContain('Do NOT infer or guess paths')
136
+ expect(block).toContain('NEVER assume a library is available')
137
+ expect(block).toContain('trust this section')
138
+
139
+ // File count
140
+ expect(block).toContain('292 files in project')
141
+ })
142
+
143
+ it('should handle minimal input', () => {
144
+ const truth: ProjectGroundTruth = {
145
+ projectPath: '/test',
146
+ }
147
+
148
+ const block = buildAntiHallucinationBlock(truth)
149
+
150
+ expect(block).toContain('CONSTRAINTS')
151
+ expect(block).toContain('SCOPE: Only files in `/test` are accessible.')
152
+ // Should not have AVAILABLE or NOT PRESENT lines
153
+ expect(block).not.toContain('AVAILABLE in this project:')
154
+ expect(block).not.toContain('NOT PRESENT:')
155
+ })
156
+
157
+ it('should not duplicate framework in techStack listing', () => {
158
+ const truth: ProjectGroundTruth = {
159
+ projectPath: '/test',
160
+ language: 'TypeScript',
161
+ framework: 'Next.js',
162
+ techStack: ['Next.js', 'React', 'Tailwind'],
163
+ }
164
+
165
+ const block = buildAntiHallucinationBlock(truth)
166
+
167
+ // Next.js should appear once (from framework), not duplicated from techStack
168
+ const matches = block.match(/Next\.js/g)
169
+ expect(matches?.length).toBe(1)
170
+ })
171
+ })
172
+
173
+ // =============================================================================
174
+ // Section Ordering
175
+ // =============================================================================
176
+
177
+ describe('Prompt Section Ordering (PRJ-301)', () => {
178
+ let builder: typeof promptBuilder
179
+
180
+ beforeEach(() => {
181
+ builder = promptBuilder
182
+ builder.resetContext()
183
+ })
184
+
185
+ it('should define correct section order constant', () => {
186
+ expect(PROMPT_SECTION_ORDER).toEqual([
187
+ 'identity',
188
+ 'environment',
189
+ 'ground_truth',
190
+ 'capabilities',
191
+ 'constraints',
192
+ 'task_context',
193
+ 'task',
194
+ 'output_schema',
195
+ 'efficiency',
196
+ ])
197
+ })
198
+
199
+ it('should place environment block before constraints', async () => {
200
+ const template = {
201
+ frontmatter: { description: 'Test', 'allowed-tools': ['Read'] },
202
+ content: '## Instructions\nDo the thing',
203
+ }
204
+ const context = { projectPath: '/test', files: ['a.js'] }
205
+
206
+ const prompt = await builder.build(template, context, {})
207
+
208
+ const envPos = prompt.indexOf('<env>')
209
+ const constraintsPos = prompt.indexOf('CONSTRAINTS')
210
+ expect(envPos).toBeGreaterThan(-1)
211
+ expect(constraintsPos).toBeGreaterThan(-1)
212
+ expect(envPos).toBeLessThan(constraintsPos)
213
+ })
214
+
215
+ it('should place constraints before task template content', async () => {
216
+ const template = {
217
+ frontmatter: { description: 'Test' },
218
+ content: '## UNIQUE_TEMPLATE_MARKER\nFollow these steps',
219
+ }
220
+ const context = { projectPath: '/test', files: ['a.js'] }
221
+
222
+ const prompt = await builder.build(template, context, {})
223
+
224
+ const constraintsPos = prompt.indexOf('CONSTRAINTS')
225
+ const templatePos = prompt.indexOf('UNIQUE_TEMPLATE_MARKER')
226
+ expect(constraintsPos).toBeGreaterThan(-1)
227
+ expect(templatePos).toBeGreaterThan(-1)
228
+ expect(constraintsPos).toBeLessThan(templatePos)
229
+ })
230
+
231
+ it('should place identity (TASK:) at the beginning', async () => {
232
+ const template = {
233
+ frontmatter: { description: 'My Task' },
234
+ content: '## Flow\nStep 1',
235
+ }
236
+ const context = { projectPath: '/test' }
237
+
238
+ const prompt = await builder.build(template, context, {})
239
+
240
+ const taskPos = prompt.indexOf('TASK: My Task')
241
+ expect(taskPos).toBeLessThan(50)
242
+ })
243
+
244
+ it('should place efficiency directive at the end', async () => {
245
+ const template = {
246
+ frontmatter: { description: 'Test' },
247
+ content: '## Flow\nStep 1',
248
+ }
249
+ const context = { projectPath: '/test' }
250
+
251
+ const prompt = await builder.build(template, context, {})
252
+
253
+ const efficiencyPos = prompt.indexOf('OUTPUT RULES')
254
+ const executePos = prompt.indexOf('EXECUTE:')
255
+ expect(efficiencyPos).toBeGreaterThan(-1)
256
+ expect(executePos).toBeGreaterThan(-1)
257
+ // Should be in the last ~300 chars of the prompt
258
+ expect(prompt.length - executePos).toBeLessThan(300)
259
+ })
260
+ })
261
+
262
+ // =============================================================================
263
+ // Token Efficiency Directive
264
+ // =============================================================================
265
+
266
+ describe('Token Efficiency Directive (PRJ-301)', () => {
267
+ let builder: typeof promptBuilder
268
+
269
+ beforeEach(() => {
270
+ builder = promptBuilder
271
+ builder.resetContext()
272
+ })
273
+
274
+ it('should include efficiency rules in every prompt', async () => {
275
+ const template = {
276
+ frontmatter: { description: 'Test' },
277
+ content: '## Flow\nStep 1',
278
+ }
279
+ const context = { projectPath: '/test' }
280
+
281
+ const prompt = await builder.build(template, context, {})
282
+
283
+ expect(prompt).toContain('OUTPUT RULES')
284
+ expect(prompt).toContain('Be concise')
285
+ expect(prompt).toContain('No preamble')
286
+ expect(prompt).toContain('No postamble')
287
+ expect(prompt).toContain('EXECUTE:')
288
+ })
289
+
290
+ it('should build efficiency directive as standalone method', () => {
291
+ const directive = builder.buildEfficiencyDirective()
292
+
293
+ expect(directive).toContain('Maximum 4 lines')
294
+ expect(directive).toContain('No preamble')
295
+ expect(directive).toContain('Prefer structured output')
296
+ expect(directive).toContain('EXECUTE:')
297
+ })
298
+ })
@@ -107,7 +107,7 @@ describe('PromptBuilder', () => {
107
107
 
108
108
  const prompt = await builder.build(template, context, state)
109
109
 
110
- expect(prompt).toContain('PATTERNS')
110
+ expect(prompt).toContain('STACK')
111
111
  expect(prompt).toContain('Node.js')
112
112
  })
113
113
 
@@ -187,7 +187,7 @@ describe('PromptBuilder', () => {
187
187
  expect(prompt).toContain('TASK:')
188
188
  expect(prompt).toContain('TOOLS:')
189
189
  expect(prompt).toContain('Flow')
190
- expect(prompt).toContain('RULES (CRITICAL)')
190
+ expect(prompt).toContain('CONSTRAINTS (Read Before Acting)')
191
191
  expect(prompt).toContain('## FILES:')
192
192
  })
193
193