@soleri/core 9.6.0 → 9.7.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/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/packs/index.d.ts +1 -1
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/types.d.ts +69 -42
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/github-projection.d.ts +3 -1
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +5 -1
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/planning/goal-ancestry.d.ts +72 -0
- package/dist/planning/goal-ancestry.d.ts.map +1 -0
- package/dist/planning/goal-ancestry.js +137 -0
- package/dist/planning/goal-ancestry.js.map +1 -0
- package/dist/planning/plan-lifecycle.d.ts +2 -0
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +1 -0
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/runtime/context-health.d.ts +14 -1
- package/dist/runtime/context-health.d.ts.map +1 -1
- package/dist/runtime/context-health.js +30 -2
- package/dist/runtime/context-health.js.map +1 -1
- package/dist/session/compaction-evaluator.d.ts +20 -0
- package/dist/session/compaction-evaluator.d.ts.map +1 -0
- package/dist/session/compaction-evaluator.js +73 -0
- package/dist/session/compaction-evaluator.js.map +1 -0
- package/dist/session/compaction-policy.d.ts +50 -0
- package/dist/session/compaction-policy.d.ts.map +1 -0
- package/dist/session/compaction-policy.js +17 -0
- package/dist/session/compaction-policy.js.map +1 -0
- package/dist/session/handoff-renderer.d.ts +22 -0
- package/dist/session/handoff-renderer.d.ts.map +1 -0
- package/dist/session/handoff-renderer.js +49 -0
- package/dist/session/handoff-renderer.js.map +1 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +5 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/policy-resolver.d.ts +20 -0
- package/dist/session/policy-resolver.d.ts.map +1 -0
- package/dist/session/policy-resolver.js +28 -0
- package/dist/session/policy-resolver.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +27 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +92 -1
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/skills/trust-classifier.d.ts +32 -0
- package/dist/skills/trust-classifier.d.ts.map +1 -0
- package/dist/skills/trust-classifier.js +109 -0
- package/dist/skills/trust-classifier.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +4 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +14 -2
- package/dist/subagent/dispatcher.js.map +1 -1
- package/package.json +1 -4
- package/src/index.ts +40 -0
- package/src/packs/index.ts +4 -0
- package/src/packs/types.ts +32 -0
- package/src/planning/github-projection.ts +6 -0
- package/src/planning/goal-ancestry.test.ts +427 -0
- package/src/planning/goal-ancestry.ts +187 -0
- package/src/planning/plan-lifecycle.ts +3 -0
- package/src/planning/planner-types.ts +2 -0
- package/src/runtime/context-health.ts +42 -2
- package/src/session/compaction-evaluator.ts +87 -0
- package/src/session/compaction-policy.ts +66 -0
- package/src/session/compaction.test.ts +259 -0
- package/src/session/handoff-renderer.ts +56 -0
- package/src/session/index.ts +12 -0
- package/src/session/policy-resolver.ts +34 -0
- package/src/skills/sync-skills.ts +114 -1
- package/src/skills/trust-classifier.test.ts +252 -0
- package/src/skills/trust-classifier.ts +127 -0
- package/src/subagent/dispatcher.ts +18 -2
package/src/index.ts
CHANGED
|
@@ -366,6 +366,16 @@ export {
|
|
|
366
366
|
} from './planning/gap-types.js';
|
|
367
367
|
export type { GapSeverity, GapCategory, PlanGap } from './planning/gap-types.js';
|
|
368
368
|
|
|
369
|
+
// ─── Goal Ancestry ──────────────────────────────────────────────────
|
|
370
|
+
export { GoalAncestry, JsonGoalRepository, generateGoalId } from './planning/goal-ancestry.js';
|
|
371
|
+
export type {
|
|
372
|
+
GoalLevel,
|
|
373
|
+
GoalStatus,
|
|
374
|
+
Goal,
|
|
375
|
+
GoalStore,
|
|
376
|
+
GoalRepository,
|
|
377
|
+
} from './planning/goal-ancestry.js';
|
|
378
|
+
|
|
369
379
|
// ─── Task Complexity Assessor ────────────────────────────────────────
|
|
370
380
|
export { assessTaskComplexity } from './planning/task-complexity-assessor.js';
|
|
371
381
|
export type {
|
|
@@ -670,8 +680,23 @@ export type {
|
|
|
670
680
|
LockfileData,
|
|
671
681
|
ResolvedPack,
|
|
672
682
|
ResolveOptions,
|
|
683
|
+
TrustLevel,
|
|
684
|
+
SourceType,
|
|
685
|
+
SkillInventoryItem,
|
|
686
|
+
SkillMetadata,
|
|
673
687
|
} from './packs/index.js';
|
|
674
688
|
|
|
689
|
+
// ─── Skill Trust & Sync ─────────────────────────────────────────────────
|
|
690
|
+
export { classifyTrust, TrustClassifier } from './skills/trust-classifier.js';
|
|
691
|
+
export {
|
|
692
|
+
discoverSkills,
|
|
693
|
+
syncSkillsToClaudeCode,
|
|
694
|
+
classifySkills,
|
|
695
|
+
checkSkillCompatibility,
|
|
696
|
+
ApprovalRequiredError,
|
|
697
|
+
} from './skills/sync-skills.js';
|
|
698
|
+
export type { SkillEntry, SyncResult, ClassifySkillsOptions } from './skills/sync-skills.js';
|
|
699
|
+
|
|
675
700
|
// ─── Plugin System ──────────────────────────────────────────────────────
|
|
676
701
|
export {
|
|
677
702
|
PluginRegistry,
|
|
@@ -840,6 +865,21 @@ export type {
|
|
|
840
865
|
OperatorProfileHistory,
|
|
841
866
|
} from './operator/operator-types.js';
|
|
842
867
|
|
|
868
|
+
// ─── Session Compaction ─────────────────────────────────────────────
|
|
869
|
+
export type {
|
|
870
|
+
CompactionPolicy,
|
|
871
|
+
CompactionResult,
|
|
872
|
+
SessionState,
|
|
873
|
+
HandoffNote,
|
|
874
|
+
} from './session/index.js';
|
|
875
|
+
export {
|
|
876
|
+
ENGINE_DEFAULTS,
|
|
877
|
+
shouldCompact,
|
|
878
|
+
parseDuration,
|
|
879
|
+
resolvePolicy,
|
|
880
|
+
renderHandoff,
|
|
881
|
+
} from './session/index.js';
|
|
882
|
+
|
|
843
883
|
// ─── Operator Context (Adaptive Persona) ────────────────────────────
|
|
844
884
|
export { OperatorContextStore } from './operator/operator-context-store.js';
|
|
845
885
|
export { DECLINED_CATEGORIES } from './operator/operator-context-types.js';
|
package/src/packs/index.ts
CHANGED
package/src/packs/types.ts
CHANGED
|
@@ -19,6 +19,38 @@
|
|
|
19
19
|
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// SKILL TRUST & SOURCE TRACKING
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/** How much a skill can do — escalates from pure docs to executable code */
|
|
27
|
+
export type TrustLevel = 'markdown_only' | 'assets' | 'scripts';
|
|
28
|
+
|
|
29
|
+
/** Where a skill came from */
|
|
30
|
+
export type SourceType = 'builtin' | 'pack' | 'local' | 'github' | 'npm';
|
|
31
|
+
|
|
32
|
+
/** A single file in a skill directory, classified by kind */
|
|
33
|
+
export interface SkillInventoryItem {
|
|
34
|
+
/** Relative path within the skill directory */
|
|
35
|
+
path: string;
|
|
36
|
+
/** What kind of file this is */
|
|
37
|
+
kind: 'skill' | 'reference' | 'asset' | 'script';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Extended metadata for a discovered skill */
|
|
41
|
+
export interface SkillMetadata {
|
|
42
|
+
/** Computed trust level based on directory contents */
|
|
43
|
+
trust: TrustLevel;
|
|
44
|
+
/** Where this skill was sourced from */
|
|
45
|
+
source: { type: SourceType; uri: string };
|
|
46
|
+
/** Engine version compatibility check result */
|
|
47
|
+
compatibility: 'compatible' | 'unknown' | 'invalid';
|
|
48
|
+
/** Engine version declared by the skill (if any) */
|
|
49
|
+
engineVersion?: string;
|
|
50
|
+
/** Classified inventory of all files in the skill directory */
|
|
51
|
+
inventory: SkillInventoryItem[];
|
|
52
|
+
}
|
|
53
|
+
|
|
22
54
|
// =============================================================================
|
|
23
55
|
// MANIFEST SCHEMA
|
|
24
56
|
// =============================================================================
|
|
@@ -333,6 +333,7 @@ export function formatIssueBody(
|
|
|
333
333
|
plan: PlanMetadataForIssue,
|
|
334
334
|
taskTitle: string,
|
|
335
335
|
taskDescription: string,
|
|
336
|
+
options?: { goalContext?: string },
|
|
336
337
|
): string {
|
|
337
338
|
const lines: string[] = [];
|
|
338
339
|
|
|
@@ -341,6 +342,11 @@ export function formatIssueBody(
|
|
|
341
342
|
lines.push(`**Objective:** ${plan.objective}`);
|
|
342
343
|
lines.push('');
|
|
343
344
|
|
|
345
|
+
if (options?.goalContext) {
|
|
346
|
+
lines.push(options.goalContext);
|
|
347
|
+
lines.push('');
|
|
348
|
+
}
|
|
349
|
+
|
|
344
350
|
if (plan.decisions.length > 0) {
|
|
345
351
|
lines.push('## Decisions');
|
|
346
352
|
for (const d of plan.decisions) {
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { GoalAncestry, JsonGoalRepository, generateGoalId } from './goal-ancestry.js';
|
|
6
|
+
import type { Goal, GoalRepository } from './goal-ancestry.js';
|
|
7
|
+
import { Planner } from './planner.js';
|
|
8
|
+
import { formatIssueBody } from './github-projection.js';
|
|
9
|
+
|
|
10
|
+
// ─── In-memory repository for unit tests ──────────────────────────
|
|
11
|
+
|
|
12
|
+
class InMemoryGoalRepository implements GoalRepository {
|
|
13
|
+
private goals: Goal[] = [];
|
|
14
|
+
|
|
15
|
+
getById(id: string): Goal | null {
|
|
16
|
+
return this.goals.find((g) => g.id === id) ?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getByParentId(parentId: string): Goal[] {
|
|
20
|
+
return this.goals.filter((g) => g.parentId === parentId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
create(goal: Omit<Goal, 'createdAt' | 'updatedAt'>): Goal {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const full: Goal = { ...goal, createdAt: now, updatedAt: now };
|
|
26
|
+
this.goals.push(full);
|
|
27
|
+
return full;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
updateStatus(id: string, status: Goal['status']): Goal {
|
|
31
|
+
const goal = this.getById(id);
|
|
32
|
+
if (!goal) throw new Error(`Goal not found: ${id}`);
|
|
33
|
+
goal.status = status;
|
|
34
|
+
goal.updatedAt = Date.now();
|
|
35
|
+
return goal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list(): Goal[] {
|
|
39
|
+
return [...this.goals];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Test helper — seed a goal directly */
|
|
43
|
+
seed(goal: Goal): void {
|
|
44
|
+
this.goals.push(goal);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('GoalAncestry', () => {
|
|
49
|
+
let repo: InMemoryGoalRepository;
|
|
50
|
+
let ancestry: GoalAncestry;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
repo = new InMemoryGoalRepository();
|
|
54
|
+
ancestry = new GoalAncestry(repo);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getAncestors', () => {
|
|
58
|
+
it('should return empty array for goal with no parent', () => {
|
|
59
|
+
repo.seed({
|
|
60
|
+
id: 'g1',
|
|
61
|
+
title: 'Root',
|
|
62
|
+
level: 'objective',
|
|
63
|
+
status: 'active',
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
updatedAt: Date.now(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ancestors = ancestry.getAncestors('g1');
|
|
69
|
+
expect(ancestors).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return one ancestor for 1-level depth', () => {
|
|
73
|
+
repo.seed({
|
|
74
|
+
id: 'root',
|
|
75
|
+
title: 'Root Objective',
|
|
76
|
+
level: 'objective',
|
|
77
|
+
status: 'active',
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
updatedAt: Date.now(),
|
|
80
|
+
});
|
|
81
|
+
repo.seed({
|
|
82
|
+
id: 'child',
|
|
83
|
+
title: 'Project A',
|
|
84
|
+
level: 'project',
|
|
85
|
+
parentId: 'root',
|
|
86
|
+
status: 'active',
|
|
87
|
+
createdAt: Date.now(),
|
|
88
|
+
updatedAt: Date.now(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const ancestors = ancestry.getAncestors('child');
|
|
92
|
+
expect(ancestors).toHaveLength(1);
|
|
93
|
+
expect(ancestors[0].id).toBe('root');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should walk 3 levels of ancestry', () => {
|
|
97
|
+
repo.seed({
|
|
98
|
+
id: 'obj',
|
|
99
|
+
title: 'Objective',
|
|
100
|
+
level: 'objective',
|
|
101
|
+
status: 'active',
|
|
102
|
+
createdAt: 1,
|
|
103
|
+
updatedAt: 1,
|
|
104
|
+
});
|
|
105
|
+
repo.seed({
|
|
106
|
+
id: 'proj',
|
|
107
|
+
title: 'Project',
|
|
108
|
+
level: 'project',
|
|
109
|
+
parentId: 'obj',
|
|
110
|
+
status: 'active',
|
|
111
|
+
createdAt: 2,
|
|
112
|
+
updatedAt: 2,
|
|
113
|
+
});
|
|
114
|
+
repo.seed({
|
|
115
|
+
id: 'plan',
|
|
116
|
+
title: 'Plan',
|
|
117
|
+
level: 'plan',
|
|
118
|
+
parentId: 'proj',
|
|
119
|
+
status: 'active',
|
|
120
|
+
createdAt: 3,
|
|
121
|
+
updatedAt: 3,
|
|
122
|
+
});
|
|
123
|
+
repo.seed({
|
|
124
|
+
id: 'task',
|
|
125
|
+
title: 'Task',
|
|
126
|
+
level: 'task',
|
|
127
|
+
parentId: 'plan',
|
|
128
|
+
status: 'planned',
|
|
129
|
+
createdAt: 4,
|
|
130
|
+
updatedAt: 4,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const ancestors = ancestry.getAncestors('task');
|
|
134
|
+
expect(ancestors).toHaveLength(3);
|
|
135
|
+
// Closest first: plan, proj, obj
|
|
136
|
+
expect(ancestors[0].id).toBe('plan');
|
|
137
|
+
expect(ancestors[1].id).toBe('proj');
|
|
138
|
+
expect(ancestors[2].id).toBe('obj');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should stop at max 10 levels', () => {
|
|
142
|
+
// Build a chain of 12 goals
|
|
143
|
+
for (let i = 0; i < 12; i++) {
|
|
144
|
+
repo.seed({
|
|
145
|
+
id: `g${i}`,
|
|
146
|
+
title: `Goal ${i}`,
|
|
147
|
+
level: 'project',
|
|
148
|
+
parentId: i > 0 ? `g${i - 1}` : undefined,
|
|
149
|
+
status: 'active',
|
|
150
|
+
createdAt: i,
|
|
151
|
+
updatedAt: i,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ancestors = ancestry.getAncestors('g11');
|
|
156
|
+
expect(ancestors.length).toBeLessThanOrEqual(10);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw on cycle detection', () => {
|
|
160
|
+
repo.seed({
|
|
161
|
+
id: 'a',
|
|
162
|
+
title: 'A',
|
|
163
|
+
level: 'project',
|
|
164
|
+
parentId: 'b',
|
|
165
|
+
status: 'active',
|
|
166
|
+
createdAt: 1,
|
|
167
|
+
updatedAt: 1,
|
|
168
|
+
});
|
|
169
|
+
repo.seed({
|
|
170
|
+
id: 'b',
|
|
171
|
+
title: 'B',
|
|
172
|
+
level: 'project',
|
|
173
|
+
parentId: 'a',
|
|
174
|
+
status: 'active',
|
|
175
|
+
createdAt: 2,
|
|
176
|
+
updatedAt: 2,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(() => ancestry.getAncestors('a')).toThrow(/[Cc]ycle/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should return empty for nonexistent goal', () => {
|
|
183
|
+
const ancestors = ancestry.getAncestors('nonexistent');
|
|
184
|
+
expect(ancestors).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('getContext', () => {
|
|
189
|
+
it('should render markdown hierarchy from root to current', () => {
|
|
190
|
+
repo.seed({
|
|
191
|
+
id: 'obj',
|
|
192
|
+
title: 'Ship v2',
|
|
193
|
+
level: 'objective',
|
|
194
|
+
status: 'active',
|
|
195
|
+
createdAt: 1,
|
|
196
|
+
updatedAt: 1,
|
|
197
|
+
});
|
|
198
|
+
repo.seed({
|
|
199
|
+
id: 'proj',
|
|
200
|
+
title: 'Auth System',
|
|
201
|
+
level: 'project',
|
|
202
|
+
parentId: 'obj',
|
|
203
|
+
status: 'active',
|
|
204
|
+
createdAt: 2,
|
|
205
|
+
updatedAt: 2,
|
|
206
|
+
});
|
|
207
|
+
repo.seed({
|
|
208
|
+
id: 'plan',
|
|
209
|
+
title: 'JWT Implementation',
|
|
210
|
+
level: 'plan',
|
|
211
|
+
parentId: 'proj',
|
|
212
|
+
status: 'active',
|
|
213
|
+
createdAt: 3,
|
|
214
|
+
updatedAt: 3,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const md = ancestry.getContext('plan');
|
|
218
|
+
expect(md).toContain('## Goal Context');
|
|
219
|
+
expect(md).toContain('[objective] Ship v2');
|
|
220
|
+
expect(md).toContain('[project] Auth System');
|
|
221
|
+
expect(md).toContain('[plan] JWT Implementation');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return empty string for nonexistent goal', () => {
|
|
225
|
+
expect(ancestry.getContext('nope')).toBe('');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should mark current goal with bold arrow', () => {
|
|
229
|
+
repo.seed({
|
|
230
|
+
id: 'obj',
|
|
231
|
+
title: 'Objective',
|
|
232
|
+
level: 'objective',
|
|
233
|
+
status: 'active',
|
|
234
|
+
createdAt: 1,
|
|
235
|
+
updatedAt: 1,
|
|
236
|
+
});
|
|
237
|
+
const md = ancestry.getContext('obj');
|
|
238
|
+
expect(md).toContain('**→**');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('inject', () => {
|
|
243
|
+
it('should add goalAncestry to config', () => {
|
|
244
|
+
repo.seed({
|
|
245
|
+
id: 'obj',
|
|
246
|
+
title: 'Ship v2',
|
|
247
|
+
level: 'objective',
|
|
248
|
+
status: 'active',
|
|
249
|
+
createdAt: 1,
|
|
250
|
+
updatedAt: 1,
|
|
251
|
+
});
|
|
252
|
+
repo.seed({
|
|
253
|
+
id: 'task',
|
|
254
|
+
title: 'Do thing',
|
|
255
|
+
level: 'task',
|
|
256
|
+
parentId: 'obj',
|
|
257
|
+
status: 'planned',
|
|
258
|
+
createdAt: 2,
|
|
259
|
+
updatedAt: 2,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const ctx = { config: { timeout: 5000 } };
|
|
263
|
+
const enriched = ancestry.inject(ctx, 'task');
|
|
264
|
+
expect(enriched.config?.goalAncestry).toContain('## Goal Context');
|
|
265
|
+
expect(enriched.config?.timeout).toBe(5000);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return original context if goal not found', () => {
|
|
269
|
+
const ctx = { config: { foo: 'bar' } };
|
|
270
|
+
const result = ancestry.inject(ctx, 'nonexistent');
|
|
271
|
+
expect(result).toEqual(ctx);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('JsonGoalRepository', () => {
|
|
277
|
+
let tempDir: string;
|
|
278
|
+
let repo: JsonGoalRepository;
|
|
279
|
+
|
|
280
|
+
beforeEach(() => {
|
|
281
|
+
tempDir = join(tmpdir(), `goal-repo-test-${Date.now()}`);
|
|
282
|
+
mkdirSync(tempDir, { recursive: true });
|
|
283
|
+
repo = new JsonGoalRepository(join(tempDir, 'goals.json'));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
afterEach(() => {
|
|
287
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should create and retrieve a goal', () => {
|
|
291
|
+
const goal = repo.create({ id: 'g1', title: 'Ship it', level: 'objective', status: 'planned' });
|
|
292
|
+
expect(goal.createdAt).toBeGreaterThan(0);
|
|
293
|
+
expect(repo.getById('g1')?.title).toBe('Ship it');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should list goals by parent', () => {
|
|
297
|
+
repo.create({ id: 'parent', title: 'Parent', level: 'objective', status: 'active' });
|
|
298
|
+
repo.create({
|
|
299
|
+
id: 'child1',
|
|
300
|
+
title: 'Child 1',
|
|
301
|
+
level: 'project',
|
|
302
|
+
parentId: 'parent',
|
|
303
|
+
status: 'planned',
|
|
304
|
+
});
|
|
305
|
+
repo.create({
|
|
306
|
+
id: 'child2',
|
|
307
|
+
title: 'Child 2',
|
|
308
|
+
level: 'project',
|
|
309
|
+
parentId: 'parent',
|
|
310
|
+
status: 'planned',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const children = repo.getByParentId('parent');
|
|
314
|
+
expect(children).toHaveLength(2);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should update status', () => {
|
|
318
|
+
repo.create({ id: 'g1', title: 'Goal', level: 'plan', status: 'planned' });
|
|
319
|
+
const updated = repo.updateStatus('g1', 'completed');
|
|
320
|
+
expect(updated.status).toBe('completed');
|
|
321
|
+
expect(repo.getById('g1')?.status).toBe('completed');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should throw when updating nonexistent goal', () => {
|
|
325
|
+
expect(() => repo.updateStatus('nope', 'active')).toThrow(/not found/);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should persist across instances', () => {
|
|
329
|
+
const filePath = join(tempDir, 'goals.json');
|
|
330
|
+
const repo1 = new JsonGoalRepository(filePath);
|
|
331
|
+
repo1.create({ id: 'g1', title: 'Persisted', level: 'objective', status: 'active' });
|
|
332
|
+
|
|
333
|
+
const repo2 = new JsonGoalRepository(filePath);
|
|
334
|
+
expect(repo2.getById('g1')?.title).toBe('Persisted');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('generateGoalId', () => {
|
|
339
|
+
it('should include the level in the ID', () => {
|
|
340
|
+
expect(generateGoalId('objective')).toMatch(/^goal-objective-/);
|
|
341
|
+
expect(generateGoalId('task')).toMatch(/^goal-task-/);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Planner goalId integration', () => {
|
|
346
|
+
let tempDir: string;
|
|
347
|
+
let planner: Planner;
|
|
348
|
+
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
tempDir = join(tmpdir(), `planner-goal-test-${Date.now()}`);
|
|
351
|
+
mkdirSync(tempDir, { recursive: true });
|
|
352
|
+
planner = new Planner(join(tempDir, 'plans.json'));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
afterEach(() => {
|
|
356
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should store goalId on created plan', () => {
|
|
360
|
+
const plan = planner.create({
|
|
361
|
+
objective: 'Add auth',
|
|
362
|
+
scope: 'backend',
|
|
363
|
+
goalId: 'goal-plan-123',
|
|
364
|
+
});
|
|
365
|
+
expect(plan.goalId).toBe('goal-plan-123');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should create plan without goalId (backward compat)', () => {
|
|
369
|
+
const plan = planner.create({ objective: 'Add auth', scope: 'backend' });
|
|
370
|
+
expect(plan.goalId).toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should preserve goalId through split', () => {
|
|
374
|
+
const plan = planner.create({
|
|
375
|
+
objective: 'Add auth',
|
|
376
|
+
scope: 'backend',
|
|
377
|
+
goalId: 'goal-plan-456',
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
planner.splitTasks(plan.id, [
|
|
381
|
+
{ title: 'JWT', description: 'Implement JWT' },
|
|
382
|
+
{ title: 'Middleware', description: 'Auth middleware' },
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
const updated = planner.get(plan.id)!;
|
|
386
|
+
expect(updated.goalId).toBe('goal-plan-456');
|
|
387
|
+
expect(updated.tasks).toHaveLength(2);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('formatIssueBody with goal context', () => {
|
|
392
|
+
it('should include goal context section when provided', () => {
|
|
393
|
+
const body = formatIssueBody(
|
|
394
|
+
{
|
|
395
|
+
planId: 'plan-1',
|
|
396
|
+
grade: 'A',
|
|
397
|
+
score: 92,
|
|
398
|
+
objective: 'Build auth',
|
|
399
|
+
decisions: [],
|
|
400
|
+
tasks: [{ id: 'task-1', title: 'JWT', description: 'Implement JWT' }],
|
|
401
|
+
},
|
|
402
|
+
'JWT',
|
|
403
|
+
'Implement JWT tokens',
|
|
404
|
+
{ goalContext: '## Goal Context\n\n- [objective] Ship v2 (active)' },
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(body).toContain('## Goal Context');
|
|
408
|
+
expect(body).toContain('[objective] Ship v2');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should not include section when no goal context', () => {
|
|
412
|
+
const body = formatIssueBody(
|
|
413
|
+
{
|
|
414
|
+
planId: 'plan-1',
|
|
415
|
+
grade: 'A',
|
|
416
|
+
score: 92,
|
|
417
|
+
objective: 'Build auth',
|
|
418
|
+
decisions: [],
|
|
419
|
+
tasks: [],
|
|
420
|
+
},
|
|
421
|
+
'JWT',
|
|
422
|
+
'Implement JWT tokens',
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
expect(body).not.toContain('## Goal Context');
|
|
426
|
+
});
|
|
427
|
+
});
|