@soleri/core 9.6.0 → 9.7.1

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 (90) hide show
  1. package/dist/index.d.ts +10 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/packs/index.d.ts +1 -1
  6. package/dist/packs/index.d.ts.map +1 -1
  7. package/dist/packs/index.js.map +1 -1
  8. package/dist/packs/types.d.ts +69 -42
  9. package/dist/packs/types.d.ts.map +1 -1
  10. package/dist/packs/types.js.map +1 -1
  11. package/dist/planning/github-projection.d.ts +3 -1
  12. package/dist/planning/github-projection.d.ts.map +1 -1
  13. package/dist/planning/github-projection.js +5 -1
  14. package/dist/planning/github-projection.js.map +1 -1
  15. package/dist/planning/goal-ancestry.d.ts +72 -0
  16. package/dist/planning/goal-ancestry.d.ts.map +1 -0
  17. package/dist/planning/goal-ancestry.js +137 -0
  18. package/dist/planning/goal-ancestry.js.map +1 -0
  19. package/dist/planning/plan-lifecycle.d.ts +2 -0
  20. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  21. package/dist/planning/plan-lifecycle.js +1 -0
  22. package/dist/planning/plan-lifecycle.js.map +1 -1
  23. package/dist/planning/planner-types.d.ts +2 -0
  24. package/dist/planning/planner-types.d.ts.map +1 -1
  25. package/dist/runtime/context-health.d.ts +14 -1
  26. package/dist/runtime/context-health.d.ts.map +1 -1
  27. package/dist/runtime/context-health.js +30 -2
  28. package/dist/runtime/context-health.js.map +1 -1
  29. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  30. package/dist/runtime/facades/orchestrate-facade.js +11 -0
  31. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  32. package/dist/session/compaction-evaluator.d.ts +20 -0
  33. package/dist/session/compaction-evaluator.d.ts.map +1 -0
  34. package/dist/session/compaction-evaluator.js +73 -0
  35. package/dist/session/compaction-evaluator.js.map +1 -0
  36. package/dist/session/compaction-policy.d.ts +50 -0
  37. package/dist/session/compaction-policy.d.ts.map +1 -0
  38. package/dist/session/compaction-policy.js +17 -0
  39. package/dist/session/compaction-policy.js.map +1 -0
  40. package/dist/session/handoff-renderer.d.ts +22 -0
  41. package/dist/session/handoff-renderer.d.ts.map +1 -0
  42. package/dist/session/handoff-renderer.js +49 -0
  43. package/dist/session/handoff-renderer.js.map +1 -0
  44. package/dist/session/index.d.ts +6 -0
  45. package/dist/session/index.d.ts.map +1 -0
  46. package/dist/session/index.js +5 -0
  47. package/dist/session/index.js.map +1 -0
  48. package/dist/session/policy-resolver.d.ts +20 -0
  49. package/dist/session/policy-resolver.d.ts.map +1 -0
  50. package/dist/session/policy-resolver.js +28 -0
  51. package/dist/session/policy-resolver.js.map +1 -0
  52. package/dist/skills/sync-skills.d.ts +27 -0
  53. package/dist/skills/sync-skills.d.ts.map +1 -1
  54. package/dist/skills/sync-skills.js +92 -1
  55. package/dist/skills/sync-skills.js.map +1 -1
  56. package/dist/skills/trust-classifier.d.ts +32 -0
  57. package/dist/skills/trust-classifier.d.ts.map +1 -0
  58. package/dist/skills/trust-classifier.js +109 -0
  59. package/dist/skills/trust-classifier.js.map +1 -0
  60. package/dist/subagent/dispatcher.d.ts +4 -0
  61. package/dist/subagent/dispatcher.d.ts.map +1 -1
  62. package/dist/subagent/dispatcher.js +14 -2
  63. package/dist/subagent/dispatcher.js.map +1 -1
  64. package/dist/update-check.d.ts +19 -0
  65. package/dist/update-check.d.ts.map +1 -1
  66. package/dist/update-check.js +51 -4
  67. package/dist/update-check.js.map +1 -1
  68. package/package.json +1 -4
  69. package/src/index.ts +44 -0
  70. package/src/packs/index.ts +4 -0
  71. package/src/packs/types.ts +32 -0
  72. package/src/planning/github-projection.ts +6 -0
  73. package/src/planning/goal-ancestry.test.ts +427 -0
  74. package/src/planning/goal-ancestry.ts +187 -0
  75. package/src/planning/plan-lifecycle.ts +3 -0
  76. package/src/planning/planner-types.ts +2 -0
  77. package/src/runtime/context-health.ts +42 -2
  78. package/src/runtime/facades/orchestrate-facade.ts +14 -0
  79. package/src/session/compaction-evaluator.ts +87 -0
  80. package/src/session/compaction-policy.ts +66 -0
  81. package/src/session/compaction.test.ts +259 -0
  82. package/src/session/handoff-renderer.ts +56 -0
  83. package/src/session/index.ts +12 -0
  84. package/src/session/policy-resolver.ts +34 -0
  85. package/src/skills/sync-skills.ts +114 -1
  86. package/src/skills/trust-classifier.test.ts +252 -0
  87. package/src/skills/trust-classifier.ts +127 -0
  88. package/src/subagent/dispatcher.ts +18 -2
  89. package/src/update-check.test.ts +91 -0
  90. package/src/update-check.ts +76 -6
@@ -1 +1 @@
1
- {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgDH;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmD3F"}
1
+ {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzD;AAgBD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,GACpB;IAAE,kBAAkB,EAAE,OAAO,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAY/D;AAkCD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C3F"}
@@ -13,6 +13,38 @@ const CACHE_FILE = join(SOLERI_HOME, 'update-check.json');
13
13
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
14
  const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@soleri/core/latest';
15
15
  const FETCH_TIMEOUT_MS = 5_000;
16
+ const CHANGELOG_BASE_URL = 'https://github.com/adrozdenko/soleri/releases/tag';
17
+ /**
18
+ * Build a changelog URL for a given version.
19
+ */
20
+ export function buildChangelogUrl(version) {
21
+ return `${CHANGELOG_BASE_URL}/v${version}`;
22
+ }
23
+ /**
24
+ * Parse the major version from a semver string (x.y.z).
25
+ */
26
+ function parseMajor(version) {
27
+ return Number(version.split('.')[0]) || 0;
28
+ }
29
+ /**
30
+ * Parse the minor version from a semver string (x.y.z).
31
+ */
32
+ function parseMinor(version) {
33
+ return Number(version.split('.')[1]) || 0;
34
+ }
35
+ /**
36
+ * Detect breaking changes (major version bump) and multi-release jumps (minor +2).
37
+ */
38
+ export function detectBreakingChanges(currentVersion, latestVersion) {
39
+ const currentMajor = parseMajor(currentVersion);
40
+ const latestMajor = parseMajor(latestVersion);
41
+ const hasBreakingChanges = latestMajor !== currentMajor;
42
+ const currentMinor = parseMinor(currentVersion);
43
+ const latestMinor = parseMinor(latestVersion);
44
+ // Multiple releases: same major but minor jumped by 2+
45
+ const hasMultipleReleases = !hasBreakingChanges && latestMajor === currentMajor && latestMinor - currentMinor >= 2;
46
+ return { hasBreakingChanges, hasMultipleReleases };
47
+ }
16
48
  function readCache() {
17
49
  try {
18
50
  if (!existsSync(CACHE_FILE))
@@ -65,8 +97,7 @@ export async function checkForUpdate(agentId, currentVersion) {
65
97
  if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
66
98
  // Use cached result
67
99
  if (cache.latestVersion && isNewer(currentVersion, cache.latestVersion)) {
68
- console.error(`${tag} Update available: @soleri/core ${currentVersion} → ${cache.latestVersion}`);
69
- console.error(`${tag} Run: soleri agent update`);
100
+ emitUpdateNotifications(tag, currentVersion, cache.latestVersion);
70
101
  }
71
102
  return;
72
103
  }
@@ -84,8 +115,7 @@ export async function checkForUpdate(agentId, currentVersion) {
84
115
  const latestVersion = data.version ?? null;
85
116
  writeCache({ checkedAt: Date.now(), latestVersion });
86
117
  if (latestVersion && isNewer(currentVersion, latestVersion)) {
87
- console.error(`${tag} Update available: @soleri/core ${currentVersion} → ${latestVersion}`);
88
- console.error(`${tag} Run: soleri agent update`);
118
+ emitUpdateNotifications(tag, currentVersion, latestVersion);
89
119
  }
90
120
  }
91
121
  catch {
@@ -93,4 +123,21 @@ export async function checkForUpdate(agentId, currentVersion) {
93
123
  writeCache({ checkedAt: Date.now(), latestVersion: null });
94
124
  }
95
125
  }
126
+ /**
127
+ * Emit update notification messages to stderr, including changelog URL
128
+ * and breaking change / multi-release warnings.
129
+ */
130
+ function emitUpdateNotifications(tag, currentVersion, latestVersion) {
131
+ console.error(`${tag} Update available: @soleri/core ${currentVersion} → ${latestVersion}`);
132
+ console.error(`${tag} Run: soleri agent update`);
133
+ console.error(`${tag} Changelog: ${buildChangelogUrl(latestVersion)}`);
134
+ const { hasBreakingChanges, hasMultipleReleases } = detectBreakingChanges(currentVersion, latestVersion);
135
+ if (hasBreakingChanges) {
136
+ const latestMajor = latestVersion.split('.')[0];
137
+ console.error(`${tag} ⚠ Breaking changes in v${latestMajor}.0.0 — see migration guide`);
138
+ }
139
+ else if (hasMultipleReleases) {
140
+ console.error(`${tag} Multiple releases since your version — review changelog`);
141
+ }
142
+ }
96
143
  //# sourceMappingURL=update-check.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/core",
3
- "version": "9.6.0",
3
+ "version": "9.7.1",
4
4
  "description": "Shared engine for Soleri agents — vault, brain, planner, LLM utilities, and facade infrastructure.",
5
5
  "keywords": [
6
6
  "agent",
@@ -55,9 +55,6 @@
55
55
  "yaml": "^2.7.0",
56
56
  "zod": "^3.24.2"
57
57
  },
58
- "optionalDependencies": {
59
- "better-sqlite3": "^11.8.1"
60
- },
61
58
  "devDependencies": {
62
59
  "@types/better-sqlite3": "^7.6.13",
63
60
  "@types/pdf-parse": "^1.1.5",
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';
@@ -860,3 +900,7 @@ export type {
860
900
  ContextItemType,
861
901
  DeclinedCategory,
862
902
  } from './operator/operator-context-types.js';
903
+
904
+ // ─── Update Check ────────────────────────────────────────────────────
905
+ export { checkForUpdate, buildChangelogUrl, detectBreakingChanges } from './update-check.js';
906
+ export type { UpdateInfo } from './update-check.js';
@@ -14,6 +14,10 @@ export {
14
14
  type InstalledPack,
15
15
  type InstallResult,
16
16
  type ValidateResult,
17
+ type TrustLevel,
18
+ type SourceType,
19
+ type SkillInventoryItem,
20
+ type SkillMetadata,
17
21
  } from './types.js';
18
22
 
19
23
  export { PackInstaller } from './pack-installer.js';
@@ -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
+ });