@soleri/core 9.3.1 → 9.5.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.
Files changed (205) hide show
  1. package/dist/brain/intelligence.d.ts +5 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +115 -26
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/brain/learning-radar.d.ts +3 -3
  6. package/dist/brain/learning-radar.d.ts.map +1 -1
  7. package/dist/brain/learning-radar.js +8 -4
  8. package/dist/brain/learning-radar.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +2 -2
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +35 -1
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/control/types.d.ts +10 -2
  14. package/dist/control/types.d.ts.map +1 -1
  15. package/dist/curator/curator.d.ts +4 -0
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +23 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/curator/schema.d.ts +1 -1
  20. package/dist/curator/schema.d.ts.map +1 -1
  21. package/dist/curator/schema.js +8 -0
  22. package/dist/curator/schema.js.map +1 -1
  23. package/dist/domain-packs/types.d.ts +6 -0
  24. package/dist/domain-packs/types.d.ts.map +1 -1
  25. package/dist/domain-packs/types.js +1 -0
  26. package/dist/domain-packs/types.js.map +1 -1
  27. package/dist/engine/module-manifest.js +3 -3
  28. package/dist/engine/module-manifest.js.map +1 -1
  29. package/dist/engine/register-engine.d.ts +9 -0
  30. package/dist/engine/register-engine.d.ts.map +1 -1
  31. package/dist/engine/register-engine.js +59 -1
  32. package/dist/engine/register-engine.js.map +1 -1
  33. package/dist/facades/types.d.ts +5 -1
  34. package/dist/facades/types.d.ts.map +1 -1
  35. package/dist/facades/types.js.map +1 -1
  36. package/dist/hooks/candidate-scorer.d.ts +28 -0
  37. package/dist/hooks/candidate-scorer.d.ts.map +1 -0
  38. package/dist/hooks/candidate-scorer.js +20 -0
  39. package/dist/hooks/candidate-scorer.js.map +1 -0
  40. package/dist/hooks/index.d.ts +2 -0
  41. package/dist/hooks/index.d.ts.map +1 -0
  42. package/dist/hooks/index.js +2 -0
  43. package/dist/hooks/index.js.map +1 -0
  44. package/dist/index.d.ts +4 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/operator/operator-context-store.d.ts +54 -0
  49. package/dist/operator/operator-context-store.d.ts.map +1 -0
  50. package/dist/operator/operator-context-store.js +434 -0
  51. package/dist/operator/operator-context-store.js.map +1 -0
  52. package/dist/operator/operator-context-types.d.ts +101 -0
  53. package/dist/operator/operator-context-types.d.ts.map +1 -0
  54. package/dist/operator/operator-context-types.js +27 -0
  55. package/dist/operator/operator-context-types.js.map +1 -0
  56. package/dist/packs/index.d.ts +2 -2
  57. package/dist/packs/index.d.ts.map +1 -1
  58. package/dist/packs/index.js +1 -1
  59. package/dist/packs/index.js.map +1 -1
  60. package/dist/packs/lockfile.d.ts +3 -0
  61. package/dist/packs/lockfile.d.ts.map +1 -1
  62. package/dist/packs/lockfile.js.map +1 -1
  63. package/dist/packs/types.d.ts +8 -2
  64. package/dist/packs/types.d.ts.map +1 -1
  65. package/dist/packs/types.js +6 -0
  66. package/dist/packs/types.js.map +1 -1
  67. package/dist/planning/plan-lifecycle.d.ts +12 -1
  68. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  69. package/dist/planning/plan-lifecycle.js +54 -16
  70. package/dist/planning/plan-lifecycle.js.map +1 -1
  71. package/dist/planning/planner-types.d.ts +6 -0
  72. package/dist/planning/planner-types.d.ts.map +1 -1
  73. package/dist/planning/planner.d.ts +21 -1
  74. package/dist/planning/planner.d.ts.map +1 -1
  75. package/dist/planning/planner.js +62 -3
  76. package/dist/planning/planner.js.map +1 -1
  77. package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
  78. package/dist/planning/task-complexity-assessor.js.map +1 -1
  79. package/dist/plugins/types.d.ts +18 -18
  80. package/dist/runtime/admin-ops.d.ts +1 -1
  81. package/dist/runtime/admin-ops.d.ts.map +1 -1
  82. package/dist/runtime/admin-ops.js +100 -3
  83. package/dist/runtime/admin-ops.js.map +1 -1
  84. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  85. package/dist/runtime/admin-setup-ops.js +19 -9
  86. package/dist/runtime/admin-setup-ops.js.map +1 -1
  87. package/dist/runtime/capture-ops.d.ts.map +1 -1
  88. package/dist/runtime/capture-ops.js +35 -7
  89. package/dist/runtime/capture-ops.js.map +1 -1
  90. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/brain-facade.js +4 -2
  92. package/dist/runtime/facades/brain-facade.js.map +1 -1
  93. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  94. package/dist/runtime/facades/control-facade.js +8 -2
  95. package/dist/runtime/facades/control-facade.js.map +1 -1
  96. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  97. package/dist/runtime/facades/curator-facade.js +13 -0
  98. package/dist/runtime/facades/curator-facade.js.map +1 -1
  99. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  100. package/dist/runtime/facades/memory-facade.js +10 -12
  101. package/dist/runtime/facades/memory-facade.js.map +1 -1
  102. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  103. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  104. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  105. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  106. package/dist/runtime/facades/plan-facade.js +20 -4
  107. package/dist/runtime/facades/plan-facade.js.map +1 -1
  108. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  109. package/dist/runtime/orchestrate-ops.js +71 -4
  110. package/dist/runtime/orchestrate-ops.js.map +1 -1
  111. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  112. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  113. package/dist/runtime/plan-feedback-helper.js +52 -0
  114. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  115. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  116. package/dist/runtime/planning-extra-ops.js +73 -34
  117. package/dist/runtime/planning-extra-ops.js.map +1 -1
  118. package/dist/runtime/session-briefing.d.ts.map +1 -1
  119. package/dist/runtime/session-briefing.js +9 -1
  120. package/dist/runtime/session-briefing.js.map +1 -1
  121. package/dist/runtime/types.d.ts +3 -0
  122. package/dist/runtime/types.d.ts.map +1 -1
  123. package/dist/skills/sync-skills.d.ts.map +1 -1
  124. package/dist/skills/sync-skills.js +13 -7
  125. package/dist/skills/sync-skills.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/brain/brain-intelligence.test.ts +30 -0
  128. package/src/brain/brain.ts +120 -46
  129. package/src/brain/extraction-quality.test.ts +323 -0
  130. package/src/brain/intelligence.ts +175 -64
  131. package/src/brain/learning-radar.ts +8 -5
  132. package/src/brain/second-brain-features.test.ts +1 -1
  133. package/src/chat/agent-loop.ts +1 -1
  134. package/src/chat/notifications.ts +4 -0
  135. package/src/control/intent-router.test.ts +73 -3
  136. package/src/control/intent-router.ts +48 -9
  137. package/src/control/types.ts +13 -2
  138. package/src/curator/curator.test.ts +92 -0
  139. package/src/curator/curator.ts +162 -18
  140. package/src/curator/schema.ts +8 -0
  141. package/src/domain-packs/types.ts +8 -0
  142. package/src/engine/module-manifest.test.ts +8 -2
  143. package/src/engine/module-manifest.ts +3 -3
  144. package/src/engine/register-engine.test.ts +73 -1
  145. package/src/engine/register-engine.ts +61 -1
  146. package/src/facades/types.ts +5 -0
  147. package/src/hooks/candidate-scorer.test.ts +76 -0
  148. package/src/hooks/candidate-scorer.ts +39 -0
  149. package/src/hooks/index.ts +6 -0
  150. package/src/index.ts +24 -0
  151. package/src/llm/llm-client.ts +1 -0
  152. package/src/operator/operator-context-store.test.ts +698 -0
  153. package/src/operator/operator-context-store.ts +569 -0
  154. package/src/operator/operator-context-types.ts +139 -0
  155. package/src/packs/index.ts +3 -1
  156. package/src/packs/lockfile.ts +3 -0
  157. package/src/packs/types.ts +9 -0
  158. package/src/persistence/sqlite-provider.ts +1 -0
  159. package/src/planning/github-projection.ts +48 -44
  160. package/src/planning/plan-lifecycle.ts +93 -22
  161. package/src/planning/planner-types.ts +6 -0
  162. package/src/planning/planner.ts +74 -4
  163. package/src/planning/task-complexity-assessor.test.ts +6 -2
  164. package/src/planning/task-complexity-assessor.ts +1 -4
  165. package/src/queue/pipeline-runner.ts +4 -0
  166. package/src/runtime/admin-ops.test.ts +139 -6
  167. package/src/runtime/admin-ops.ts +104 -3
  168. package/src/runtime/admin-setup-ops.ts +30 -10
  169. package/src/runtime/capture-ops.test.ts +84 -0
  170. package/src/runtime/capture-ops.ts +35 -7
  171. package/src/runtime/curator-extra-ops.test.ts +7 -0
  172. package/src/runtime/curator-extra-ops.ts +10 -1
  173. package/src/runtime/facades/admin-facade.test.ts +1 -1
  174. package/src/runtime/facades/brain-facade.ts +6 -3
  175. package/src/runtime/facades/control-facade.ts +10 -2
  176. package/src/runtime/facades/curator-facade.test.ts +7 -0
  177. package/src/runtime/facades/curator-facade.ts +18 -0
  178. package/src/runtime/facades/memory-facade.test.ts +14 -12
  179. package/src/runtime/facades/memory-facade.ts +197 -12
  180. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  181. package/src/runtime/facades/plan-facade.test.ts +213 -0
  182. package/src/runtime/facades/plan-facade.ts +23 -4
  183. package/src/runtime/orchestrate-ops.test.ts +202 -2
  184. package/src/runtime/orchestrate-ops.ts +88 -7
  185. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  186. package/src/runtime/plan-feedback-helper.ts +63 -0
  187. package/src/runtime/planning-extra-ops.test.ts +43 -1
  188. package/src/runtime/planning-extra-ops.ts +96 -33
  189. package/src/runtime/runtime.test.ts +50 -2
  190. package/src/runtime/runtime.ts +117 -89
  191. package/src/runtime/session-briefing.test.ts +1 -0
  192. package/src/runtime/session-briefing.ts +10 -1
  193. package/src/runtime/shutdown-registry.test.ts +151 -0
  194. package/src/runtime/shutdown-registry.ts +85 -0
  195. package/src/runtime/types.ts +7 -1
  196. package/src/skills/sync-skills.ts +14 -7
  197. package/src/transport/http-server.ts +50 -3
  198. package/src/transport/ws-server.ts +8 -0
  199. package/src/vault/linking.test.ts +12 -0
  200. package/src/vault/linking.ts +90 -44
  201. package/src/vault/vault-maintenance.ts +11 -18
  202. package/src/vault/vault-memories.ts +21 -13
  203. package/src/vault/vault-schema.ts +21 -0
  204. package/src/vault/vault.ts +8 -3
  205. package/vitest.config.ts +1 -0
@@ -4,7 +4,9 @@
4
4
 
5
5
  export {
6
6
  packManifestSchema,
7
+ PACK_TIERS,
7
8
  type PackManifest,
9
+ type PackTier as ManifestPackTier,
8
10
  type PackStatus,
9
11
  type InstalledPack,
10
12
  type InstallResult,
@@ -14,7 +16,7 @@ export {
14
16
  export { PackInstaller } from './pack-installer.js';
15
17
 
16
18
  export { PackLockfile, inferPackType } from './lockfile.js';
17
- export type { LockEntry, PackType, PackSource, LockfileData } from './lockfile.js';
19
+ export type { LockEntry, PackType, PackSource, PackTier, LockfileData } from './lockfile.js';
18
20
 
19
21
  export { resolvePack, checkNpmVersion, checkVersionCompat } from './resolver.js';
20
22
  export type { ResolvedPack, ResolveOptions } from './resolver.js';
@@ -39,10 +39,13 @@ export interface LockEntry {
39
39
  facadesRegistered: boolean;
40
40
  /** Compatible @soleri/core version range (from manifest "soleri" field) */
41
41
  soleriRange?: string;
42
+ /** Pack tier: default (ships with engine), community (free), premium (unlocked today) */
43
+ tier?: PackTier;
42
44
  }
43
45
 
44
46
  export type PackType = 'hooks' | 'skills' | 'knowledge' | 'domain' | 'bundle';
45
47
  export type PackSource = 'built-in' | 'local' | 'npm';
48
+ export type PackTier = 'default' | 'community' | 'premium';
46
49
 
47
50
  export interface LockfileData {
48
51
  /** Lockfile format version */
@@ -23,11 +23,20 @@ import { z } from 'zod';
23
23
  // MANIFEST SCHEMA
24
24
  // =============================================================================
25
25
 
26
+ // ---------------------------------------------------------------------------
27
+ // Pack Tiers — determines visibility, licensing, and install behavior
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export const PACK_TIERS = ['default', 'community', 'premium'] as const;
31
+ export type PackTier = (typeof PACK_TIERS)[number];
32
+
26
33
  export const packManifestSchema = z.object({
27
34
  id: z.string().regex(/^[a-z0-9-]+$/, 'Pack ID must be lowercase alphanumeric with hyphens'),
28
35
  name: z.string().min(1),
29
36
  version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver (x.y.z)'),
30
37
  description: z.string().optional().default(''),
38
+ /** Pack tier: 'default' (ships with engine), 'community' (free, npm), 'premium' (unlocked today, gated later) */
39
+ tier: z.enum(PACK_TIERS).optional().default('community'),
31
40
  /** Domains this pack covers */
32
41
  domains: z.array(z.string()).optional().default([]),
33
42
  /** Minimum engine version required (semver range) */
@@ -20,6 +20,7 @@ export function applyPerformancePragmas(db: Database.Database): void {
20
20
  db.pragma('cache_size = -64000'); // 64MB
21
21
  db.pragma('temp_store = MEMORY');
22
22
  db.pragma('mmap_size = 268435456'); // 256MB
23
+ db.pragma('synchronous = NORMAL');
23
24
  }
24
25
 
25
26
  export class SQLitePersistenceProvider implements PersistenceProvider {
@@ -7,7 +7,10 @@
7
7
  * The plan is the source of truth; GitHub issues are the projection.
8
8
  */
9
9
 
10
- import { execFileSync } from 'node:child_process';
10
+ import { execFile } from 'node:child_process';
11
+ import { promisify } from 'node:util';
12
+
13
+ const execFileAsync = promisify(execFile);
11
14
 
12
15
  // ---------------------------------------------------------------------------
13
16
  // Types
@@ -88,15 +91,14 @@ export function parseGitHubRemote(remoteUrl: string): GitHubRepo | null {
88
91
  * Detect the GitHub remote from a project directory.
89
92
  * Returns null if no GitHub remote found or not a git repo.
90
93
  */
91
- export function detectGitHubRemote(projectPath: string): GitHubRepo | null {
94
+ export async function detectGitHubRemote(projectPath: string): Promise<GitHubRepo | null> {
92
95
  try {
93
- const output = execFileSync('git', ['remote', 'get-url', 'origin'], {
96
+ const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
94
97
  cwd: projectPath,
95
- encoding: 'utf-8',
96
98
  timeout: 5000,
97
- stdio: ['pipe', 'pipe', 'pipe'],
98
- }).trim();
99
- return parseGitHubRemote(output);
99
+ signal: AbortSignal.timeout(5000),
100
+ });
101
+ return parseGitHubRemote(stdout.trim());
100
102
  } catch {
101
103
  return null;
102
104
  }
@@ -105,12 +107,11 @@ export function detectGitHubRemote(projectPath: string): GitHubRepo | null {
105
107
  /**
106
108
  * Check if the `gh` CLI is authenticated.
107
109
  */
108
- export function isGhAuthenticated(): boolean {
110
+ export async function isGhAuthenticated(): Promise<boolean> {
109
111
  try {
110
- execFileSync('gh', ['auth', 'status'], {
111
- encoding: 'utf-8',
112
+ await execFileAsync('gh', ['auth', 'status'], {
112
113
  timeout: 5000,
113
- stdio: ['pipe', 'pipe', 'pipe'],
114
+ signal: AbortSignal.timeout(5000),
114
115
  });
115
116
  return true;
116
117
  } catch {
@@ -125,9 +126,9 @@ export function isGhAuthenticated(): boolean {
125
126
  /**
126
127
  * List milestones for a GitHub repo.
127
128
  */
128
- export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
129
+ export async function listMilestones(repo: GitHubRepo): Promise<GitHubMilestone[]> {
129
130
  try {
130
- const output = execFileSync(
131
+ const { stdout } = await execFileAsync(
131
132
  'gh',
132
133
  [
133
134
  'api',
@@ -136,12 +137,12 @@ export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
136
137
  '.[] | {number, title, state}',
137
138
  ],
138
139
  {
139
- encoding: 'utf-8',
140
140
  timeout: 10000,
141
- stdio: ['pipe', 'pipe', 'pipe'],
141
+ signal: AbortSignal.timeout(10000),
142
142
  },
143
- ).trim();
143
+ );
144
144
 
145
+ const output = stdout.trim();
145
146
  if (!output) return [];
146
147
 
147
148
  // gh --jq outputs one JSON object per line
@@ -157,9 +158,12 @@ export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
157
158
  /**
158
159
  * List open issues for a GitHub repo.
159
160
  */
160
- export function listOpenIssues(repo: GitHubRepo, limit: number = 100): GitHubIssue[] {
161
+ export async function listOpenIssues(
162
+ repo: GitHubRepo,
163
+ limit: number = 100,
164
+ ): Promise<GitHubIssue[]> {
161
165
  try {
162
- const output = execFileSync(
166
+ const { stdout } = await execFileAsync(
163
167
  'gh',
164
168
  [
165
169
  'issue',
@@ -174,12 +178,12 @@ export function listOpenIssues(repo: GitHubRepo, limit: number = 100): GitHubIss
174
178
  'number,title,state,body',
175
179
  ],
176
180
  {
177
- encoding: 'utf-8',
178
181
  timeout: 10000,
179
- stdio: ['pipe', 'pipe', 'pipe'],
182
+ signal: AbortSignal.timeout(10000),
180
183
  },
181
- ).trim();
184
+ );
182
185
 
186
+ const output = stdout.trim();
183
187
  if (!output) return [];
184
188
  return JSON.parse(output) as GitHubIssue[];
185
189
  } catch {
@@ -190,18 +194,18 @@ export function listOpenIssues(repo: GitHubRepo, limit: number = 100): GitHubIss
190
194
  /**
191
195
  * List labels for a GitHub repo.
192
196
  */
193
- export function listLabels(repo: GitHubRepo): GitHubLabel[] {
197
+ export async function listLabels(repo: GitHubRepo): Promise<GitHubLabel[]> {
194
198
  try {
195
- const output = execFileSync(
199
+ const { stdout } = await execFileAsync(
196
200
  'gh',
197
201
  ['label', 'list', '--repo', `${repo.owner}/${repo.repo}`, '--json', 'name,color'],
198
202
  {
199
- encoding: 'utf-8',
200
203
  timeout: 10000,
201
- stdio: ['pipe', 'pipe', 'pipe'],
204
+ signal: AbortSignal.timeout(10000),
202
205
  },
203
- ).trim();
206
+ );
204
207
 
208
+ const output = stdout.trim();
205
209
  if (!output) return [];
206
210
  return JSON.parse(output) as GitHubLabel[];
207
211
  } catch {
@@ -217,16 +221,18 @@ export function listLabels(repo: GitHubRepo): GitHubLabel[] {
217
221
  * Detect full GitHub context for a project.
218
222
  * Returns null if not a GitHub project or gh CLI not available.
219
223
  */
220
- export function detectGitHubContext(projectPath: string): GitHubContext | null {
221
- const repo = detectGitHubRemote(projectPath);
224
+ export async function detectGitHubContext(projectPath: string): Promise<GitHubContext | null> {
225
+ const repo = await detectGitHubRemote(projectPath);
222
226
  if (!repo) return null;
223
227
 
224
- const authenticated = isGhAuthenticated();
228
+ const authenticated = await isGhAuthenticated();
225
229
  if (!authenticated) return null;
226
230
 
227
- const milestones = listMilestones(repo);
228
- const existingIssues = listOpenIssues(repo);
229
- const labels = listLabels(repo);
231
+ const [milestones, existingIssues, labels] = await Promise.all([
232
+ listMilestones(repo),
233
+ listOpenIssues(repo),
234
+ listLabels(repo),
235
+ ]);
230
236
 
231
237
  return { repo, authenticated, milestones, existingIssues, labels };
232
238
  }
@@ -370,7 +376,7 @@ export function formatIssueBody(
370
376
  * Create a GitHub issue using the `gh` CLI.
371
377
  * Returns the issue number, or null on failure.
372
378
  */
373
- export function createGitHubIssue(
379
+ export async function createGitHubIssue(
374
380
  repo: GitHubRepo,
375
381
  title: string,
376
382
  body: string,
@@ -378,7 +384,7 @@ export function createGitHubIssue(
378
384
  milestone?: number;
379
385
  labels?: string[];
380
386
  },
381
- ): number | null {
387
+ ): Promise<number | null> {
382
388
  try {
383
389
  const args = [
384
390
  'issue',
@@ -399,14 +405,13 @@ export function createGitHubIssue(
399
405
  args.push('--label', options.labels.join(','));
400
406
  }
401
407
 
402
- const output = execFileSync('gh', args, {
403
- encoding: 'utf-8',
408
+ const { stdout } = await execFileAsync('gh', args, {
404
409
  timeout: 15000,
405
- stdio: ['pipe', 'pipe', 'pipe'],
406
- }).trim();
410
+ signal: AbortSignal.timeout(15000),
411
+ });
407
412
 
408
413
  // gh issue create returns the issue URL: https://github.com/owner/repo/issues/123
409
- const match = output.match(/\/issues\/(\d+)/);
414
+ const match = stdout.trim().match(/\/issues\/(\d+)/);
410
415
  return match ? parseInt(match[1], 10) : null;
411
416
  } catch {
412
417
  return null;
@@ -416,13 +421,13 @@ export function createGitHubIssue(
416
421
  /**
417
422
  * Update an existing GitHub issue body (for linking plans to existing issues).
418
423
  */
419
- export function updateGitHubIssueBody(
424
+ export async function updateGitHubIssueBody(
420
425
  repo: GitHubRepo,
421
426
  issueNumber: number,
422
427
  body: string,
423
- ): boolean {
428
+ ): Promise<boolean> {
424
429
  try {
425
- execFileSync(
430
+ await execFileAsync(
426
431
  'gh',
427
432
  [
428
433
  'issue',
@@ -434,9 +439,8 @@ export function updateGitHubIssueBody(
434
439
  body,
435
440
  ],
436
441
  {
437
- encoding: 'utf-8',
438
442
  timeout: 15000,
439
- stdio: ['pipe', 'pipe', 'pipe'],
443
+ signal: AbortSignal.timeout(15000),
440
444
  },
441
445
  );
442
446
  return true;
@@ -232,7 +232,13 @@ export interface IterateChanges {
232
232
  objective?: string;
233
233
  scope?: string;
234
234
  decisions?: (string | PlanDecision)[];
235
- addTasks?: Array<{ title: string; description: string }>;
235
+ addTasks?: Array<{
236
+ title: string;
237
+ description: string;
238
+ phase?: string;
239
+ milestone?: string;
240
+ parentTaskId?: string;
241
+ }>;
236
242
  removeTasks?: string[];
237
243
  approach?: string;
238
244
  context?: string;
@@ -240,26 +246,61 @@ export interface IterateChanges {
240
246
  tool_chain?: string[];
241
247
  flow?: string;
242
248
  target_mode?: string;
249
+ alternatives?: import('./planner-types.js').PlanAlternative[];
243
250
  }
244
251
 
245
252
  /**
246
253
  * Apply iteration changes to a plan (mutates in place).
247
254
  * Caller is responsible for status validation and persistence.
255
+ * Returns the number of fields actually mutated (0 = no-op).
248
256
  */
249
- export function applyIteration(plan: Plan, changes: IterateChanges): void {
257
+ export function applyIteration(plan: Plan, changes: IterateChanges): number {
250
258
  const now = Date.now();
251
- if (changes.objective !== undefined) plan.objective = changes.objective;
252
- if (changes.scope !== undefined) plan.scope = changes.scope;
253
- if (changes.decisions !== undefined) plan.decisions = changes.decisions;
254
- if (changes.approach !== undefined) plan.approach = changes.approach;
255
- if (changes.context !== undefined) plan.context = changes.context;
256
- if (changes.success_criteria !== undefined) plan.success_criteria = changes.success_criteria;
257
- if (changes.tool_chain !== undefined) plan.tool_chain = changes.tool_chain;
258
- if (changes.flow !== undefined) plan.flow = changes.flow;
259
- if (changes.target_mode !== undefined) plan.target_mode = changes.target_mode;
259
+ let mutated = 0;
260
+ if (changes.objective !== undefined) {
261
+ plan.objective = changes.objective;
262
+ mutated++;
263
+ }
264
+ if (changes.scope !== undefined) {
265
+ plan.scope = changes.scope;
266
+ mutated++;
267
+ }
268
+ if (changes.decisions !== undefined) {
269
+ plan.decisions = changes.decisions;
270
+ mutated++;
271
+ }
272
+ if (changes.approach !== undefined) {
273
+ plan.approach = changes.approach;
274
+ mutated++;
275
+ }
276
+ if (changes.context !== undefined) {
277
+ plan.context = changes.context;
278
+ mutated++;
279
+ }
280
+ if (changes.success_criteria !== undefined) {
281
+ plan.success_criteria = changes.success_criteria;
282
+ mutated++;
283
+ }
284
+ if (changes.tool_chain !== undefined) {
285
+ plan.tool_chain = changes.tool_chain;
286
+ mutated++;
287
+ }
288
+ if (changes.flow !== undefined) {
289
+ plan.flow = changes.flow;
290
+ mutated++;
291
+ }
292
+ if (changes.target_mode !== undefined) {
293
+ plan.target_mode = changes.target_mode;
294
+ mutated++;
295
+ }
296
+ if (changes.alternatives !== undefined) {
297
+ plan.alternatives = changes.alternatives;
298
+ mutated++;
299
+ }
260
300
  if (changes.removeTasks?.length) {
261
301
  const removeSet = new Set(changes.removeTasks);
262
302
  plan.tasks = plan.tasks.filter((t) => !removeSet.has(t.id));
303
+ mutated++;
263
304
  }
264
305
  if (changes.addTasks?.length) {
265
306
  const maxIndex = plan.tasks.reduce((max, t) => {
@@ -267,16 +308,24 @@ export function applyIteration(plan: Plan, changes: IterateChanges): void {
267
308
  return isNaN(num) ? max : Math.max(max, num);
268
309
  }, 0);
269
310
  for (let i = 0; i < changes.addTasks.length; i++) {
311
+ const t = changes.addTasks[i];
270
312
  plan.tasks.push({
271
313
  id: `task-${maxIndex + i + 1}`,
272
- title: changes.addTasks[i].title,
273
- description: changes.addTasks[i].description,
314
+ title: t.title,
315
+ description: t.description,
274
316
  status: 'pending' as TaskStatus,
317
+ ...(t.phase !== undefined && { phase: t.phase }),
318
+ ...(t.milestone !== undefined && { milestone: t.milestone }),
319
+ ...(t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId }),
275
320
  updatedAt: now,
276
321
  });
277
322
  }
323
+ mutated++;
278
324
  }
279
- plan.updatedAt = now;
325
+ if (mutated > 0) {
326
+ plan.updatedAt = now;
327
+ }
328
+ return mutated;
280
329
  }
281
330
 
282
331
  /**
@@ -308,7 +357,13 @@ export function createPlanObject(params: {
308
357
  objective: string;
309
358
  scope: string;
310
359
  decisions?: (string | PlanDecision)[];
311
- tasks?: Array<{ title: string; description: string }>;
360
+ tasks?: Array<{
361
+ title: string;
362
+ description: string;
363
+ phase?: string;
364
+ milestone?: string;
365
+ parentTaskId?: string;
366
+ }>;
312
367
  approach?: string;
313
368
  context?: string;
314
369
  success_criteria?: string[];
@@ -325,13 +380,20 @@ export function createPlanObject(params: {
325
380
  scope: params.scope,
326
381
  status: params.initialStatus ?? 'draft',
327
382
  decisions: params.decisions ?? [],
328
- tasks: (params.tasks ?? []).map((t, i) => ({
329
- id: `task-${i + 1}`,
330
- title: t.title,
331
- description: t.description,
332
- status: 'pending' as TaskStatus,
333
- updatedAt: now,
334
- })),
383
+ tasks: (params.tasks ?? []).map((t, i) =>
384
+ Object.assign(
385
+ {
386
+ id: `task-${i + 1}`,
387
+ title: t.title,
388
+ description: t.description,
389
+ status: `pending` as TaskStatus,
390
+ },
391
+ t.phase !== undefined && { phase: t.phase },
392
+ t.milestone !== undefined && { milestone: t.milestone },
393
+ t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId },
394
+ { updatedAt: now },
395
+ ),
396
+ ),
335
397
  ...(params.approach !== undefined && { approach: params.approach }),
336
398
  ...(params.context !== undefined && { context: params.context }),
337
399
  ...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
@@ -352,6 +414,9 @@ export function applySplitTasks(
352
414
  description: string;
353
415
  dependsOn?: string[];
354
416
  acceptanceCriteria?: string[];
417
+ phase?: string;
418
+ milestone?: string;
419
+ parentTaskId?: string;
355
420
  }>,
356
421
  ): void {
357
422
  const now = Date.now();
@@ -362,6 +427,9 @@ export function applySplitTasks(
362
427
  status: 'pending' as TaskStatus,
363
428
  dependsOn: t.dependsOn,
364
429
  ...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
430
+ ...(t.phase !== undefined && { phase: t.phase }),
431
+ ...(t.milestone !== undefined && { milestone: t.milestone }),
432
+ ...(t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId }),
365
433
  updatedAt: now,
366
434
  }));
367
435
  const taskIds = new Set(plan.tasks.map((t) => t.id));
@@ -372,6 +440,9 @@ export function applySplitTasks(
372
440
  throw new Error(`Task '${task.id}' depends on unknown task '${dep}'`);
373
441
  }
374
442
  }
443
+ if (task.parentTaskId && !taskIds.has(task.parentTaskId)) {
444
+ throw new Error(`Task '${task.id}' references unknown parent task '${task.parentTaskId}'`);
445
+ }
375
446
  }
376
447
  plan.updatedAt = now;
377
448
  }
@@ -66,6 +66,12 @@ export interface PlanTask {
66
66
  status: TaskStatus;
67
67
  /** Optional dependency IDs — tasks that must complete before this one. */
68
68
  dependsOn?: string[];
69
+ /** Phase this task belongs to (e.g., "wave-1", "discovery", "implementation"). */
70
+ phase?: string;
71
+ /** Milestone this task contributes to (e.g., "v1.0", "mvp", "beta"). */
72
+ milestone?: string;
73
+ /** Parent task ID — enables sub-task hierarchy within a plan. */
74
+ parentTaskId?: string;
69
75
  /** Evidence submitted for task acceptance criteria. */
70
76
  evidence?: TaskEvidence[];
71
77
  /** Whether this task has been verified (all evidence checked + reviews passed). */
@@ -235,15 +235,17 @@ export class Planner {
235
235
  );
236
236
  }
237
237
 
238
- iterate(planId: string, changes: IterateChanges): Plan {
238
+ iterate(planId: string, changes: IterateChanges): { plan: Plan; mutated: number } {
239
239
  const plan = this.requirePlan(planId);
240
240
  if (plan.status !== 'draft' && plan.status !== 'brainstorming')
241
241
  throw new Error(
242
242
  `Cannot iterate plan in '${plan.status}' status — must be 'draft' or 'brainstorming'`,
243
243
  );
244
- applyIteration(plan, changes);
245
- this.save();
246
- return plan;
244
+ const mutated = applyIteration(plan, changes);
245
+ if (mutated > 0) {
246
+ this.save();
247
+ }
248
+ return { plan, mutated };
247
249
  }
248
250
 
249
251
  splitTasks(
@@ -253,6 +255,9 @@ export class Planner {
253
255
  description: string;
254
256
  dependsOn?: string[];
255
257
  acceptanceCriteria?: string[];
258
+ phase?: string;
259
+ milestone?: string;
260
+ parentTaskId?: string;
256
261
  }>,
257
262
  ): Plan {
258
263
  const plan = this.requirePlan(planId);
@@ -445,6 +450,71 @@ export class Planner {
445
450
  return toArchive;
446
451
  }
447
452
 
453
+ /**
454
+ * Close stale plans — plans in non-terminal states older than the given threshold.
455
+ * For draft/approved: uses 30 min TTL by default.
456
+ * For executing/reconciling: uses olderThanMs parameter (24h default).
457
+ * Returns the list of closed plan IDs.
458
+ */
459
+ closeStale(olderThanMs?: number): {
460
+ closedIds: string[];
461
+ closedPlans: Array<{ id: string; previousStatus: string; reason: string }>;
462
+ } {
463
+ const now = Date.now();
464
+ const forceAll = olderThanMs === 0;
465
+ const defaultTtl = forceAll ? 0 : 30 * 60 * 1000; // 30 minutes for draft/approved
466
+ const executingTtl = forceAll ? 0 : (olderThanMs ?? 24 * 60 * 60 * 1000); // 24h default for executing/reconciling
467
+ const closed: Array<{ id: string; previousStatus: string; reason: string }> = [];
468
+
469
+ for (const plan of this.store.plans) {
470
+ // Skip terminal states
471
+ if (plan.status === 'completed' || plan.status === 'archived') continue;
472
+
473
+ const age = now - plan.updatedAt;
474
+ let shouldClose = false;
475
+ let reason = '';
476
+
477
+ if (plan.status === 'draft' || plan.status === 'approved') {
478
+ // Short TTL for draft/approved — these should move quickly
479
+ if (age >= defaultTtl) {
480
+ shouldClose = true;
481
+ reason = `ttl-expired (${plan.status}, age: ${Math.round(age / 60000)}min)`;
482
+ }
483
+ } else if (
484
+ plan.status === 'executing' ||
485
+ plan.status === 'validating' ||
486
+ plan.status === 'reconciling' ||
487
+ plan.status === 'brainstorming'
488
+ ) {
489
+ // Longer TTL for active states
490
+ if (age >= executingTtl) {
491
+ shouldClose = true;
492
+ reason = `stale-closed (${plan.status}, age: ${Math.round(age / 3600000)}h)`;
493
+ }
494
+ }
495
+
496
+ if (shouldClose) {
497
+ const previousStatus = plan.status;
498
+ // Force transition to completed (bypass FSM since these are stale)
499
+ plan.status = 'completed';
500
+ plan.updatedAt = now;
501
+ if (!plan.reconciliation) {
502
+ plan.reconciliation = {
503
+ planId: plan.id,
504
+ accuracy: 0,
505
+ driftItems: [],
506
+ summary: `Auto-closed: ${reason}`,
507
+ reconciledAt: now,
508
+ };
509
+ }
510
+ closed.push({ id: plan.id, previousStatus, reason });
511
+ }
512
+ }
513
+
514
+ if (closed.length > 0) this.save();
515
+ return { closedIds: closed.map((c) => c.id), closedPlans: closed };
516
+ }
517
+
448
518
  stats(): {
449
519
  total: number;
450
520
  byStatus: Record<PlanStatus, number>;
@@ -72,7 +72,10 @@ describe('assessTaskComplexity — complex tasks', () => {
72
72
  });
73
73
 
74
74
  it('classifies many-file task with design decision as complex', () => {
75
- const result = assess({ prompt: 'how should we update styles across the app', filesEstimated: 5 });
75
+ const result = assess({
76
+ prompt: 'how should we update styles across the app',
77
+ filesEstimated: 5,
78
+ });
76
79
  expect(result.classification).toBe('complex');
77
80
  expect(result.score).toBeGreaterThanOrEqual(40);
78
81
  expect(signalByName(result, 'file-count')!.triggered).toBe(true);
@@ -117,7 +120,8 @@ describe('assessTaskComplexity — edge cases', () => {
117
120
 
118
121
  it('clamps score to 100 maximum', () => {
119
122
  const result = assess({
120
- prompt: 'add authentication, migrate the DB, install new package, how should we design this, refactor across all modules',
123
+ prompt:
124
+ 'add authentication, migrate the DB, install new package, how should we design this, refactor across all modules',
121
125
  filesEstimated: 10,
122
126
  domains: ['vault', 'brain', 'planning'],
123
127
  });
@@ -164,10 +164,7 @@ export function assessTaskComplexity(input: AssessmentInput): AssessmentResult {
164
164
  detectMultiDomain(input),
165
165
  ];
166
166
 
167
- const rawScore = signals.reduce(
168
- (sum, s) => sum + (s.triggered ? s.weight : 0),
169
- 0,
170
- );
167
+ const rawScore = signals.reduce((sum, s) => sum + (s.triggered ? s.weight : 0), 0);
171
168
 
172
169
  // Clamp to 0-100
173
170
  const score = Math.max(0, Math.min(100, rawScore));
@@ -58,6 +58,10 @@ export class PipelineRunner {
58
58
  if (this.running) return;
59
59
  this.running = true;
60
60
  this.timer = setInterval(() => this.tick(), this.pollIntervalMs);
61
+ // Don't prevent process exit for background polling
62
+ if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
63
+ (this.timer as NodeJS.Timeout).unref();
64
+ }
61
65
  }
62
66
 
63
67
  /**