@soleri/core 9.5.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/adapters/claude-code-adapter.d.ts +27 -0
- package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code-adapter.js +111 -0
- package/dist/adapters/claude-code-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/registry.d.ts +21 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +44 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +10 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/brain/brain.d.ts +12 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +106 -44
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +36 -30
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/chat/agent-loop.js +1 -1
- package/dist/chat/agent-loop.js.map +1 -1
- package/dist/chat/notifications.d.ts.map +1 -1
- package/dist/chat/notifications.js +4 -0
- package/dist/chat/notifications.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +11 -5
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +141 -27
- package/dist/curator/curator.js.map +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +1 -0
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/packs/index.d.ts +3 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +3 -2
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +23 -1
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js +50 -4
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/pack-installer.d.ts +10 -0
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +69 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/packs/pack-lifecycle.d.ts +50 -0
- package/dist/packs/pack-lifecycle.d.ts.map +1 -0
- package/dist/packs/pack-lifecycle.js +91 -0
- package/dist/packs/pack-lifecycle.js.map +1 -0
- package/dist/packs/types.d.ts +76 -29
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +9 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/persistence/sqlite-provider.d.ts +5 -1
- package/dist/persistence/sqlite-provider.d.ts.map +1 -1
- package/dist/persistence/sqlite-provider.js +22 -2
- package/dist/persistence/sqlite-provider.js.map +1 -1
- package/dist/planning/github-projection.d.ts +11 -9
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +47 -43
- 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/plugins/types.d.ts +21 -21
- package/dist/queue/pipeline-runner.d.ts.map +1 -1
- package/dist/queue/pipeline-runner.js +4 -0
- package/dist/queue/pipeline-runner.js.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/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +9 -1
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +169 -0
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +133 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +128 -90
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +44 -11
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/shutdown-registry.d.ts +36 -0
- package/dist/runtime/shutdown-registry.d.ts.map +1 -0
- package/dist/runtime/shutdown-registry.js +74 -0
- package/dist/runtime/shutdown-registry.js.map +1 -0
- package/dist/runtime/types.d.ts +10 -1
- package/dist/runtime/types.d.ts.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/concurrency-manager.d.ts +29 -0
- package/dist/subagent/concurrency-manager.d.ts.map +1 -0
- package/dist/subagent/concurrency-manager.js +73 -0
- package/dist/subagent/concurrency-manager.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +45 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -0
- package/dist/subagent/dispatcher.js +271 -0
- package/dist/subagent/dispatcher.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +15 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/orphan-reaper.d.ts +37 -0
- package/dist/subagent/orphan-reaper.d.ts.map +1 -0
- package/dist/subagent/orphan-reaper.js +71 -0
- package/dist/subagent/orphan-reaper.js.map +1 -0
- package/dist/subagent/result-aggregator.d.ts +7 -0
- package/dist/subagent/result-aggregator.d.ts.map +1 -0
- package/dist/subagent/result-aggregator.js +57 -0
- package/dist/subagent/result-aggregator.js.map +1 -0
- package/dist/subagent/task-checkout.d.ts +36 -0
- package/dist/subagent/task-checkout.d.ts.map +1 -0
- package/dist/subagent/task-checkout.js +52 -0
- package/dist/subagent/task-checkout.js.map +1 -0
- package/dist/subagent/types.d.ts +114 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +9 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/workspace-resolver.d.ts +35 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -0
- package/dist/subagent/workspace-resolver.js +99 -0
- package/dist/subagent/workspace-resolver.js.map +1 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +49 -3
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/ws-server.d.ts.map +1 -1
- package/dist/transport/ws-server.js +7 -0
- package/dist/transport/ws-server.js.map +1 -1
- package/dist/vault/linking.d.ts +3 -4
- package/dist/vault/linking.d.ts.map +1 -1
- package/dist/vault/linking.js +79 -32
- package/dist/vault/linking.js.map +1 -1
- package/dist/vault/vault-maintenance.d.ts.map +1 -1
- package/dist/vault/vault-maintenance.js +7 -14
- package/dist/vault/vault-maintenance.js.map +1 -1
- package/dist/vault/vault-memories.d.ts.map +1 -1
- package/dist/vault/vault-memories.js +19 -9
- package/dist/vault/vault-memories.js.map +1 -1
- package/dist/vault/vault-schema.d.ts +1 -0
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +20 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +7 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
- package/src/__tests__/adapters/registry.test.ts +100 -0
- package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
- package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
- package/src/__tests__/subagent/dispatcher.test.ts +195 -0
- package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
- package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
- package/src/__tests__/subagent/task-checkout.test.ts +86 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
- package/src/adapters/claude-code-adapter.ts +163 -0
- package/src/adapters/index.ts +22 -0
- package/src/adapters/registry.ts +53 -0
- package/src/adapters/types.ts +114 -0
- package/src/curator/curator.ts +1 -0
- package/src/index.ts +78 -1
- package/src/packs/index.ts +9 -1
- package/src/packs/lockfile.ts +70 -5
- package/src/packs/pack-installer.ts +78 -2
- package/src/packs/pack-lifecycle.ts +115 -0
- package/src/packs/pack-lockfile.test.ts +1 -1
- package/src/packs/pack-system.test.ts +1 -1
- package/src/packs/types.ts +72 -2
- package/src/persistence/sqlite-provider.ts +26 -2
- 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/admin-setup-ops.test.ts +9 -4
- package/src/runtime/context-health.ts +42 -2
- package/src/runtime/orchestrate-ops.ts +153 -1
- package/src/runtime/runtime.ts +15 -0
- package/src/runtime/session-briefing.test.ts +94 -2
- package/src/runtime/session-briefing.ts +48 -12
- package/src/runtime/types.ts +6 -0
- 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/concurrency-manager.ts +89 -0
- package/src/subagent/dispatcher.ts +342 -0
- package/src/subagent/index.ts +28 -0
- package/src/subagent/orphan-reaper.ts +82 -0
- package/src/subagent/result-aggregator.ts +66 -0
- package/src/subagent/task-checkout.ts +60 -0
- package/src/subagent/types.ts +138 -0
- package/src/subagent/workspace-resolver.ts +117 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/vitest.config.ts +2 -0
- package/src/hooks/index.ts +0 -6
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Ancestry — hierarchical goal tracking for plans and tasks.
|
|
3
|
+
*
|
|
4
|
+
* Goals form a tree: objective → project → plan → task.
|
|
5
|
+
* Each plan/task can reference its parent goal, enabling context
|
|
6
|
+
* to flow from high-level objectives down to individual work items.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Types ────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type GoalLevel = 'objective' | 'project' | 'plan' | 'task';
|
|
12
|
+
|
|
13
|
+
export type GoalStatus = 'planned' | 'active' | 'completed' | 'abandoned';
|
|
14
|
+
|
|
15
|
+
export interface Goal {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
level: GoalLevel;
|
|
19
|
+
parentId?: string;
|
|
20
|
+
status: GoalStatus;
|
|
21
|
+
createdAt?: number;
|
|
22
|
+
updatedAt?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Goal Store ───────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface GoalStore {
|
|
28
|
+
version: string;
|
|
29
|
+
goals: Goal[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Persistent goal repository backed by a JSON file.
|
|
34
|
+
* Follows the same pattern as PlanStore in planner.ts.
|
|
35
|
+
*/
|
|
36
|
+
export interface GoalRepository {
|
|
37
|
+
getById(id: string): Goal | null;
|
|
38
|
+
getByParentId(parentId: string): Goal[];
|
|
39
|
+
create(goal: Omit<Goal, 'createdAt' | 'updatedAt'>): Goal;
|
|
40
|
+
updateStatus(id: string, status: GoalStatus): Goal;
|
|
41
|
+
list(): Goal[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
45
|
+
import { dirname } from 'node:path';
|
|
46
|
+
|
|
47
|
+
export class JsonGoalRepository implements GoalRepository {
|
|
48
|
+
private store: GoalStore;
|
|
49
|
+
|
|
50
|
+
constructor(private filePath: string) {
|
|
51
|
+
this.store = this.load();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private load(): GoalStore {
|
|
55
|
+
if (!existsSync(this.filePath)) return { version: '1.0', goals: [] };
|
|
56
|
+
try {
|
|
57
|
+
const data = readFileSync(this.filePath, 'utf-8');
|
|
58
|
+
return JSON.parse(data) as GoalStore;
|
|
59
|
+
} catch {
|
|
60
|
+
return { version: '1.0', goals: [] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private save(): void {
|
|
65
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
66
|
+
writeFileSync(this.filePath, JSON.stringify(this.store, null, 2), 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getById(id: string): Goal | null {
|
|
70
|
+
return this.store.goals.find((g) => g.id === id) ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getByParentId(parentId: string): Goal[] {
|
|
74
|
+
return this.store.goals.filter((g) => g.parentId === parentId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
create(goal: Omit<Goal, 'createdAt' | 'updatedAt'>): Goal {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const full: Goal = { ...goal, createdAt: now, updatedAt: now };
|
|
80
|
+
this.store.goals.push(full);
|
|
81
|
+
this.save();
|
|
82
|
+
return full;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
updateStatus(id: string, status: GoalStatus): Goal {
|
|
86
|
+
const goal = this.getById(id);
|
|
87
|
+
if (!goal) throw new Error(`Goal not found: ${id}`);
|
|
88
|
+
goal.status = status;
|
|
89
|
+
goal.updatedAt = Date.now();
|
|
90
|
+
this.save();
|
|
91
|
+
return goal;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
list(): Goal[] {
|
|
95
|
+
return [...this.store.goals];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Max ancestor depth ──────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const MAX_ANCESTOR_DEPTH = 10;
|
|
102
|
+
|
|
103
|
+
// ─── GoalAncestry ────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export class GoalAncestry {
|
|
106
|
+
constructor(private repo: GoalRepository) {}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Walk the parent chain from a goal up to the root.
|
|
110
|
+
* Returns ancestors from immediate parent to root (closest first).
|
|
111
|
+
* Max 10 levels; throws on cycle detection.
|
|
112
|
+
*/
|
|
113
|
+
getAncestors(goalId: string): Goal[] {
|
|
114
|
+
const ancestors: Goal[] = [];
|
|
115
|
+
const visited = new Set<string>();
|
|
116
|
+
let currentId: string | undefined = goalId;
|
|
117
|
+
|
|
118
|
+
// Start by finding the goal itself to get its parentId
|
|
119
|
+
const start = this.repo.getById(goalId);
|
|
120
|
+
if (!start) return [];
|
|
121
|
+
currentId = start.parentId;
|
|
122
|
+
|
|
123
|
+
while (currentId && ancestors.length < MAX_ANCESTOR_DEPTH) {
|
|
124
|
+
if (visited.has(currentId)) {
|
|
125
|
+
throw new Error(`Cycle detected in goal hierarchy at goal '${currentId}'`);
|
|
126
|
+
}
|
|
127
|
+
visited.add(currentId);
|
|
128
|
+
|
|
129
|
+
const parent = this.repo.getById(currentId);
|
|
130
|
+
if (!parent) break;
|
|
131
|
+
|
|
132
|
+
ancestors.push(parent);
|
|
133
|
+
currentId = parent.parentId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ancestors;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Render a markdown summary of the goal hierarchy for a given goal.
|
|
141
|
+
* Shows the full chain from root objective down to the current goal.
|
|
142
|
+
*/
|
|
143
|
+
getContext(goalId: string): string {
|
|
144
|
+
const goal = this.repo.getById(goalId);
|
|
145
|
+
if (!goal) return '';
|
|
146
|
+
|
|
147
|
+
const ancestors = this.getAncestors(goalId);
|
|
148
|
+
// Build chain from root to current: reverse ancestors then append current
|
|
149
|
+
const chain = [...ancestors].reverse();
|
|
150
|
+
chain.push(goal);
|
|
151
|
+
|
|
152
|
+
const lines: string[] = ['## Goal Context', ''];
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < chain.length; i++) {
|
|
155
|
+
const g = chain[i];
|
|
156
|
+
const indent = ' '.repeat(i);
|
|
157
|
+
const marker = i === chain.length - 1 ? '**→**' : '-';
|
|
158
|
+
lines.push(`${indent}${marker} [${g.level}] ${g.title} (${g.status})`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Inject goal ancestry context into an execution context metadata object.
|
|
166
|
+
* Returns a new context with goalAncestry added to config.
|
|
167
|
+
*/
|
|
168
|
+
inject<T extends { config?: Record<string, unknown> }>(ctx: T, goalId: string): T {
|
|
169
|
+
const rendered = this.getContext(goalId);
|
|
170
|
+
if (!rendered) return ctx;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
...ctx,
|
|
174
|
+
config: {
|
|
175
|
+
...ctx.config,
|
|
176
|
+
goalAncestry: rendered,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate a goal ID with the given level prefix.
|
|
184
|
+
*/
|
|
185
|
+
export function generateGoalId(level: GoalLevel): string {
|
|
186
|
+
return `goal-${level}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
187
|
+
}
|
|
@@ -372,6 +372,8 @@ export function createPlanObject(params: {
|
|
|
372
372
|
target_mode?: string;
|
|
373
373
|
alternatives?: import('./planner-types.js').PlanAlternative[];
|
|
374
374
|
initialStatus?: 'brainstorming' | 'draft';
|
|
375
|
+
/** Optional goal ID to link this plan to the goal hierarchy. */
|
|
376
|
+
goalId?: string;
|
|
375
377
|
}): Plan {
|
|
376
378
|
const now = Date.now();
|
|
377
379
|
return {
|
|
@@ -401,6 +403,7 @@ export function createPlanObject(params: {
|
|
|
401
403
|
...(params.flow !== undefined && { flow: params.flow }),
|
|
402
404
|
...(params.target_mode !== undefined && { target_mode: params.target_mode }),
|
|
403
405
|
...(params.alternatives !== undefined && { alternatives: params.alternatives }),
|
|
406
|
+
...(params.goalId !== undefined && { goalId: params.goalId }),
|
|
404
407
|
checks: [],
|
|
405
408
|
createdAt: now,
|
|
406
409
|
updatedAt: now,
|
|
@@ -205,6 +205,8 @@ export interface Plan {
|
|
|
205
205
|
};
|
|
206
206
|
/** Aggregate execution metrics — populated by reconcile() and complete(). */
|
|
207
207
|
executionSummary?: ExecutionSummary;
|
|
208
|
+
/** Goal ID linking this plan to the goal hierarchy. */
|
|
209
|
+
goalId?: string;
|
|
208
210
|
createdAt: number;
|
|
209
211
|
updatedAt: number;
|
|
210
212
|
}
|
|
@@ -5,17 +5,21 @@ import type { OpDefinition } from '../facades/types.js';
|
|
|
5
5
|
|
|
6
6
|
// ─── Mock Node.js fs/os modules ────────────────────────────────────────
|
|
7
7
|
|
|
8
|
+
/** Normalize path separators so Windows backslash paths match forward-slash keys */
|
|
9
|
+
const norm = (p: string): string => p.replace(/\\/g, '/');
|
|
10
|
+
|
|
8
11
|
const mockFs: Record<string, string> = {};
|
|
9
12
|
const mockDirs = new Set<string>();
|
|
10
13
|
|
|
11
14
|
vi.mock('node:fs', () => ({
|
|
12
|
-
existsSync: vi.fn((p: string) => p in mockFs || mockDirs.has(p)),
|
|
15
|
+
existsSync: vi.fn((p: string) => norm(p) in mockFs || mockDirs.has(norm(p))),
|
|
13
16
|
readFileSync: vi.fn((p: string) => {
|
|
14
|
-
|
|
17
|
+
const key = norm(p);
|
|
18
|
+
if (key in mockFs) return mockFs[key];
|
|
15
19
|
throw new Error(`ENOENT: ${p}`);
|
|
16
20
|
}),
|
|
17
21
|
writeFileSync: vi.fn((p: string, content: string) => {
|
|
18
|
-
mockFs[p] = content;
|
|
22
|
+
mockFs[norm(p)] = content;
|
|
19
23
|
}),
|
|
20
24
|
mkdirSync: vi.fn((_p: string) => undefined),
|
|
21
25
|
copyFileSync: vi.fn(),
|
|
@@ -30,7 +34,8 @@ vi.mock('node:os', () => ({
|
|
|
30
34
|
|
|
31
35
|
vi.mock('node:path', async () => {
|
|
32
36
|
const actual = await vi.importActual<typeof import('node:path')>('node:path');
|
|
33
|
-
|
|
37
|
+
// Always use posix path semantics so mock filesystem keys (forward slashes) work on all platforms
|
|
38
|
+
return { ...actual.posix, default: actual.posix };
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
vi.mock('./claude-md-helpers.js', () => ({
|