@applica-software-guru/sdd-core 1.8.2 → 1.8.3

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 (51) hide show
  1. package/dist/agent/agent-defaults.js +2 -2
  2. package/dist/agent/agent-defaults.js.map +1 -1
  3. package/dist/agent/agent-runner.d.ts +17 -0
  4. package/dist/agent/agent-runner.d.ts.map +1 -1
  5. package/dist/agent/agent-runner.js +163 -11
  6. package/dist/agent/agent-runner.js.map +1 -1
  7. package/dist/agent/worker-daemon.d.ts +12 -0
  8. package/dist/agent/worker-daemon.d.ts.map +1 -0
  9. package/dist/agent/worker-daemon.js +220 -0
  10. package/dist/agent/worker-daemon.js.map +1 -0
  11. package/dist/git/git.d.ts +6 -0
  12. package/dist/git/git.d.ts.map +1 -1
  13. package/dist/git/git.js +60 -0
  14. package/dist/git/git.js.map +1 -1
  15. package/dist/index.d.ts +7 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +17 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/prompt/draft-prompt-generator.d.ts.map +1 -1
  20. package/dist/prompt/draft-prompt-generator.js +4 -0
  21. package/dist/prompt/draft-prompt-generator.js.map +1 -1
  22. package/dist/prompt/prompt-generator.d.ts.map +1 -1
  23. package/dist/prompt/prompt-generator.js +5 -0
  24. package/dist/prompt/prompt-generator.js.map +1 -1
  25. package/dist/remote/sync-engine.d.ts.map +1 -1
  26. package/dist/remote/sync-engine.js +1 -0
  27. package/dist/remote/sync-engine.js.map +1 -1
  28. package/dist/remote/worker-client.d.ts +22 -0
  29. package/dist/remote/worker-client.d.ts.map +1 -0
  30. package/dist/remote/worker-client.js +88 -0
  31. package/dist/remote/worker-client.js.map +1 -0
  32. package/dist/remote/worker-types.d.ts +38 -0
  33. package/dist/remote/worker-types.d.ts.map +1 -0
  34. package/dist/remote/worker-types.js +3 -0
  35. package/dist/remote/worker-types.js.map +1 -0
  36. package/dist/scaffold/templates.generated.d.ts +1 -1
  37. package/dist/scaffold/templates.generated.d.ts.map +1 -1
  38. package/dist/scaffold/templates.generated.js +51 -33
  39. package/dist/scaffold/templates.generated.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/agent/agent-defaults.ts +2 -2
  42. package/src/agent/agent-runner.ts +184 -12
  43. package/src/agent/worker-daemon.ts +254 -0
  44. package/src/git/git.ts +61 -0
  45. package/src/index.ts +7 -2
  46. package/src/prompt/draft-prompt-generator.ts +8 -0
  47. package/src/prompt/prompt-generator.ts +8 -0
  48. package/src/remote/sync-engine.ts +1 -0
  49. package/src/remote/worker-client.ts +141 -0
  50. package/src/remote/worker-types.ts +40 -0
  51. package/src/scaffold/templates.generated.ts +51 -33
package/src/git/git.ts CHANGED
@@ -85,6 +85,67 @@ export function getGitModifiedFiles(root: string): Set<string> {
85
85
  return modified;
86
86
  }
87
87
 
88
+ export function getCurrentBranch(root: string): string | null {
89
+ try {
90
+ return run('git rev-parse --abbrev-ref HEAD', root) || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ export function checkoutBranch(root: string, branch: string): void {
97
+ try {
98
+ run(`git checkout ${branch}`, root);
99
+ } catch {
100
+ // branch doesn't exist — create it
101
+ run(`git checkout -b ${branch}`, root);
102
+ }
103
+ }
104
+
105
+ export function getJobChangedFiles(
106
+ root: string,
107
+ fromCommit: string | null,
108
+ ): Array<{ path: string; status: 'new' | 'modified' | 'deleted' }> {
109
+ const result = new Map<string, 'new' | 'modified' | 'deleted'>();
110
+
111
+ try {
112
+ // Committed changes since the base commit
113
+ if (fromCommit) {
114
+ const committed = run(`git diff --name-status ${fromCommit} HEAD`, root);
115
+ for (const line of committed.split('\n').filter(Boolean)) {
116
+ const parts = line.split('\t');
117
+ const flag = parts[0][0]; // first char (A/M/D/R/C)
118
+ // Renames have two paths: old\tnew — use the new path
119
+ const path = parts.length >= 3 ? parts[2] : parts[1];
120
+ const fileStatus: 'new' | 'modified' | 'deleted' =
121
+ flag === 'A' ? 'new' : flag === 'D' ? 'deleted' : 'modified';
122
+ result.set(path, fileStatus);
123
+ }
124
+ }
125
+
126
+ // Uncommitted working tree changes (staged + unstaged + untracked)
127
+ const porcelain = run('git status --porcelain', root);
128
+ for (const line of porcelain.split('\n').filter(Boolean)) {
129
+ const xy = line.substring(0, 2);
130
+ const rawPath = line.substring(3).trim();
131
+ // Renamed files show as "old -> new" in porcelain v1
132
+ const path = rawPath.includes(' -> ') ? rawPath.split(' -> ')[1] : rawPath;
133
+
134
+ if (xy[0] === 'D' || xy[1] === 'D') {
135
+ result.set(path, 'deleted');
136
+ } else if (xy === '??' || xy[0] === 'A' || xy[1] === 'A') {
137
+ if (!result.has(path)) result.set(path, 'new');
138
+ } else {
139
+ if (!result.has(path)) result.set(path, 'modified');
140
+ }
141
+ }
142
+ } catch {
143
+ // not a git repo or no commits — return empty
144
+ }
145
+
146
+ return Array.from(result.entries()).map(([path, status]) => ({ path, status }));
147
+ }
148
+
88
149
  export function getChangedFiles(root: string, fromCommit: string | null): Array<{ path: string; status: 'new' | 'modified' | 'deleted' }> {
89
150
  try {
90
151
  if (!fromCommit) {
package/src/index.ts CHANGED
@@ -21,8 +21,11 @@ export type {
21
21
  export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError, RemoteError, RemoteNotConfiguredError, RemoteTimeoutError } from "./errors.js";
22
22
  export type { ProjectInfo } from "./scaffold/templates.js";
23
23
  export { isSDDProject, readConfig, writeConfig } from "./config/config-manager.js";
24
- export { runAgent } from "./agent/agent-runner.js";
25
- export type { AgentRunnerOptions } from "./agent/agent-runner.js";
24
+ export { getCurrentBranch, checkoutBranch } from "./git/git.js";
25
+ export { runAgent, startAgent } from "./agent/agent-runner.js";
26
+ export type { AgentRunnerOptions, AgentRunnerHandle } from "./agent/agent-runner.js";
27
+ export { startWorkerDaemon } from "./agent/worker-daemon.js";
28
+ export type { WorkerDaemonOptions } from "./agent/worker-daemon.js";
26
29
  export { DEFAULT_AGENTS, resolveAgentCommand } from "./agent/agent-defaults.js";
27
30
  export { listSupportedAdapters, SKILL_ADAPTERS, syncSkillAdapters } from "./scaffold/skill-adapters.js";
28
31
  export type {
@@ -39,6 +42,8 @@ export { generateDraftEnrichmentPrompt } from "./prompt/draft-prompt-generator.j
39
42
  export type { DraftElements } from "./prompt/draft-prompt-generator.js";
40
43
  export { resolveApiKey, buildApiConfig, pullDocs, pushDocs, pullPendingCRs, pullOpenBugs, markCRAppliedRemote, markBugResolvedRemote, markDocEnriched, markCREnriched, markBugEnriched, resetProject, DEFAULT_REMOTE_TIMEOUT } from "./remote/api-client.js";
41
44
  export type { ApiClientConfig } from "./remote/api-client.js";
45
+ export { registerWorker, workerHeartbeat, workerPoll, workerJobStarted, workerJobOutput, workerJobQuestion, workerJobAnswers, workerJobCompleted } from "./remote/worker-client.js";
46
+ export type { WorkerRegistration, WorkerJobAssignment, WorkerJobAnswer, WorkerState } from "./remote/worker-types.js";
42
47
  export { readRemoteState, writeRemoteState } from "./remote/state.js";
43
48
  export { pushToRemote, pullFromRemote, pullCRsFromRemote, pullBugsFromRemote, getRemoteStatus, resetRemoteProject } from "./remote/sync-engine.js";
44
49
  export type {
@@ -50,5 +50,13 @@ export function generateDraftEnrichmentPrompt(
50
50
  sections.push('');
51
51
  }
52
52
 
53
+ sections.push(
54
+ '## Report\n',
55
+ 'At the end of the enrichment, provide a detailed report including:\n' +
56
+ '- List of enriched files (documents, CRs, bugs)\n' +
57
+ '- For each file: what was added or completed\n' +
58
+ '- Any ambiguities resolved or assumptions made'
59
+ );
60
+
53
61
  return sections.join('\n');
54
62
  }
@@ -35,5 +35,13 @@ export function generatePrompt(files: StoryFile[], root?: string): string {
35
35
  sections.push(delLines.join('\n'));
36
36
  }
37
37
 
38
+ sections.push(
39
+ '## Report\n\n' +
40
+ 'At the end of your work, provide a detailed report including:\n' +
41
+ '- List of files created, modified, or deleted\n' +
42
+ '- Description of actions taken for each file\n' +
43
+ '- Any issues encountered or decisions made'
44
+ );
45
+
38
46
  return sections.join('\n\n');
39
47
  }
@@ -139,6 +139,7 @@ export async function pushToRemote(root: string, options?: PushOptions): Promise
139
139
  path: normalizePath(f.relativePath),
140
140
  title: f.frontmatter.title,
141
141
  content: f.body,
142
+ status: f.frontmatter.status,
142
143
  }));
143
144
 
144
145
  const result = await pushDocs(api, documents);
@@ -0,0 +1,141 @@
1
+ import type { ApiClientConfig } from './api-client.js';
2
+ import type { WorkerRegistration, WorkerJobAssignment, WorkerJobAnswer } from './worker-types.js';
3
+
4
+ /**
5
+ * Low-level fetch wrapper for worker endpoints.
6
+ * Unlike the main api-client, worker requests have different timeout needs
7
+ * (e.g. long-poll uses 35s timeout) and don't need full retry logic.
8
+ */
9
+ async function workerRequest<T>(
10
+ config: ApiClientConfig,
11
+ method: string,
12
+ path: string,
13
+ body?: unknown,
14
+ timeoutMs?: number,
15
+ ): Promise<T> {
16
+ const url = `${config.baseUrl}${path}`;
17
+ const controller = new AbortController();
18
+ const timer = setTimeout(() => controller.abort(), timeoutMs ?? 10_000);
19
+
20
+ try {
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers: {
24
+ Authorization: `Bearer ${config.apiKey}`,
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ body: body != null ? JSON.stringify(body) : undefined,
28
+ signal: controller.signal,
29
+ });
30
+ clearTimeout(timer);
31
+
32
+ if (!res.ok) {
33
+ let detail: string;
34
+ try {
35
+ const err = (await res.json()) as { detail?: string };
36
+ detail = err.detail ?? res.statusText;
37
+ } catch {
38
+ detail = res.statusText;
39
+ }
40
+ throw new Error(`Worker API error ${res.status}: ${detail}`);
41
+ }
42
+
43
+ // 204 No Content (poll with no job)
44
+ if (res.status === 204) {
45
+ return null as T;
46
+ }
47
+
48
+ return (await res.json()) as T;
49
+ } catch (error) {
50
+ clearTimeout(timer);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /** POST /cli/workers/register */
56
+ export async function registerWorker(
57
+ config: ApiClientConfig,
58
+ name: string,
59
+ agent: string,
60
+ branch?: string,
61
+ metadata?: Record<string, unknown>,
62
+ ): Promise<WorkerRegistration> {
63
+ return workerRequest<WorkerRegistration>(
64
+ config, 'POST', '/cli/workers/register',
65
+ { name, agent, branch, metadata },
66
+ );
67
+ }
68
+
69
+ /** POST /cli/workers/:workerId/heartbeat */
70
+ export async function workerHeartbeat(
71
+ config: ApiClientConfig,
72
+ workerId: string,
73
+ workerStatus: 'online' | 'busy' = 'online',
74
+ ): Promise<void> {
75
+ await workerRequest(
76
+ config, 'POST', `/cli/workers/${workerId}/heartbeat`,
77
+ { status: workerStatus },
78
+ );
79
+ }
80
+
81
+ /** GET /cli/workers/:workerId/poll — long-poll (up to 35s timeout) */
82
+ export async function workerPoll(
83
+ config: ApiClientConfig,
84
+ workerId: string,
85
+ ): Promise<WorkerJobAssignment | null> {
86
+ return workerRequest<WorkerJobAssignment | null>(
87
+ config, 'GET', `/cli/workers/${workerId}/poll`,
88
+ undefined,
89
+ 35_000, // 35s to exceed server's 30s hold time
90
+ );
91
+ }
92
+
93
+ /** POST /cli/workers/jobs/:jobId/started */
94
+ export async function workerJobStarted(
95
+ config: ApiClientConfig,
96
+ jobId: string,
97
+ ): Promise<void> {
98
+ await workerRequest(config, 'POST', `/cli/workers/jobs/${jobId}/started`);
99
+ }
100
+
101
+ /** POST /cli/workers/jobs/:jobId/output */
102
+ export async function workerJobOutput(
103
+ config: ApiClientConfig,
104
+ jobId: string,
105
+ lines: string[],
106
+ ): Promise<void> {
107
+ await workerRequest(config, 'POST', `/cli/workers/jobs/${jobId}/output`, { lines });
108
+ }
109
+
110
+ /** POST /cli/workers/jobs/:jobId/question */
111
+ export async function workerJobQuestion(
112
+ config: ApiClientConfig,
113
+ jobId: string,
114
+ content: string,
115
+ ): Promise<void> {
116
+ await workerRequest(config, 'POST', `/cli/workers/jobs/${jobId}/question`, { content });
117
+ }
118
+
119
+ /** GET /cli/workers/jobs/:jobId/answers?after_sequence=N */
120
+ export async function workerJobAnswers(
121
+ config: ApiClientConfig,
122
+ jobId: string,
123
+ afterSequence: number = 0,
124
+ ): Promise<WorkerJobAnswer[]> {
125
+ return workerRequest<WorkerJobAnswer[]>(
126
+ config, 'GET', `/cli/workers/jobs/${jobId}/answers?after_sequence=${afterSequence}`,
127
+ );
128
+ }
129
+
130
+ /** POST /cli/workers/jobs/:jobId/completed */
131
+ export async function workerJobCompleted(
132
+ config: ApiClientConfig,
133
+ jobId: string,
134
+ exitCode: number,
135
+ changedFiles?: Array<{ path: string; status: 'new' | 'modified' | 'deleted' }>,
136
+ ): Promise<void> {
137
+ await workerRequest(config, 'POST', `/cli/workers/jobs/${jobId}/completed`, {
138
+ exit_code: exitCode,
139
+ changed_files: changedFiles ?? [],
140
+ });
141
+ }
@@ -0,0 +1,40 @@
1
+ /** Worker registration response from the server */
2
+ export interface WorkerRegistration {
3
+ id: string;
4
+ project_id: string;
5
+ name: string;
6
+ status: string;
7
+ agent: string;
8
+ branch?: string;
9
+ last_heartbeat_at: string | null;
10
+ registered_at: string;
11
+ is_online: boolean;
12
+ }
13
+
14
+ /** Job assignment returned by the poll endpoint */
15
+ export interface WorkerJobAssignment {
16
+ job_id: string;
17
+ entity_type: string | null;
18
+ entity_id: string | null;
19
+ prompt: string;
20
+ agent: string;
21
+ model?: string;
22
+ branch?: string;
23
+ }
24
+
25
+ /** Answer message from the user */
26
+ export interface WorkerJobAnswer {
27
+ id: string;
28
+ job_id: string;
29
+ kind: 'answer';
30
+ content: string;
31
+ sequence: number;
32
+ created_at: string;
33
+ }
34
+
35
+ /** Local worker state persisted in .sdd/worker.json */
36
+ export interface WorkerState {
37
+ workerId: string;
38
+ name: string;
39
+ registeredAt: string;
40
+ }
@@ -228,13 +228,14 @@ name: sdd-remote
228
228
  description: >
229
229
  Remote sync workflow for Story Driven Development. Use when the user asks
230
230
  to update local state from remote changes, process remote drafts, and push
231
- enriched items back.
231
+ enriched items back. Also applies when running a remote worker job (enrich
232
+ or sync).
232
233
  license: MIT
233
234
  compatibility: Requires sdd CLI (npm i -g @applica-software-guru/sdd)
234
235
  allowed-tools: Bash(sdd:*) Read Glob Grep
235
236
  metadata:
236
237
  author: applica-software-guru
237
- version: "1.0"
238
+ version: "1.1"
238
239
  ---
239
240
 
240
241
  # SDD Remote - Pull, Enrich, Push
@@ -244,6 +245,9 @@ metadata:
244
245
  Use this skill to synchronize local SDD docs with remote updates, enrich draft content,
245
246
  and publish the enriched result to remote in active states.
246
247
 
248
+ This skill also applies when a **remote worker job** is dispatched from SDD Flow, as the
249
+ worker runs these same workflows on behalf of the user.
250
+
247
251
  ## Detection
248
252
 
249
253
  This workflow applies when:
@@ -251,79 +255,91 @@ This workflow applies when:
251
255
  - \`.sdd/config.yaml\` exists in the project root
252
256
  - The user asks to update local state from remote, pull pending CRs/bugs/docs,
253
257
  enrich drafts, or push pending remote updates
258
+ - A remote worker job prompt instructs you to follow this workflow
254
259
 
255
- ## Workflow
260
+ ## Workflows
261
+
262
+ ### Enrich Workflow (CR)
256
263
 
257
- Follow this sequence in order:
264
+ Follow this sequence to enrich a draft Change Request:
258
265
 
259
- 1. Verify remote configuration
266
+ 1. Pull remote updates:
260
267
 
261
268
  \`\`\`bash
262
- test -f .sdd/config.yaml && sdd remote status
269
+ sdd pull --crs-only
263
270
  \`\`\`
264
271
 
265
- If remote is not configured or disconnected, run:
272
+ 3. Generate draft TODO list:
266
273
 
267
274
  \`\`\`bash
268
- sdd remote init
275
+ sdd drafts
269
276
  \`\`\`
270
277
 
271
- Then stop and ask the user for URL/API key if needed.
278
+ 4. Enrich the draft with technical details, acceptance criteria, edge cases, and
279
+ any relevant information from the project documentation and comments.
272
280
 
273
- 2. Pull remote updates (docs + CRs + bugs)
281
+ 5. Transition the enriched CR to pending:
274
282
 
275
283
  \`\`\`bash
276
- sdd pull
284
+ sdd mark-drafts-enriched
277
285
  \`\`\`
278
286
 
279
- Optional scoped pulls:
287
+ This performs: \`draft → pending\`
288
+
289
+ 6. Push the enriched content:
280
290
 
281
291
  \`\`\`bash
282
- sdd pull --docs-only
283
- sdd pull --crs-only
284
- sdd pull --bugs-only
292
+ sdd push
285
293
  \`\`\`
286
294
 
287
- 3. Generate draft TODO list for your coding agent
295
+ ### Enrich Workflow (Document)
296
+
297
+ Follow this sequence to enrich a document:
298
+
299
+ 1. Pull remote updates:
288
300
 
289
301
  \`\`\`bash
290
- sdd drafts
302
+ sdd pull --docs-only
291
303
  \`\`\`
292
304
 
293
- This command lists all local draft docs, CRs, and bugs and prints a minimal TODO-style prompt.
294
- Give that prompt to your coding agent. If additional context is needed, the agent can fetch it directly from project files.
305
+ 3. Locate the document file in \`product/\` or \`system/\` and update its content
306
+ with the enriched version.
295
307
 
296
- 4. Transition enriched drafts to active states
308
+ 4. Push the enriched content:
297
309
 
298
310
  \`\`\`bash
299
- sdd mark-drafts-enriched
311
+ sdd push
300
312
  \`\`\`
301
313
 
302
- This performs:
314
+ If the document was in \`draft\` status, it will transition to \`new\` on the server.
315
+
316
+ ### Sync Workflow (Project-level)
303
317
 
304
- - Document: \`draft -> new\`
305
- - Change Request: \`draft -> pending\`
306
- - Bug: \`draft -> open\`
318
+ Follow this sequence for a full project sync (all pending items):
307
319
 
308
- 5. Push local pending updates to remote
320
+ 1. Pull the latest specs:
309
321
 
310
322
  \`\`\`bash
311
- sdd push
323
+ sdd pull
312
324
  \`\`\`
313
325
 
314
- 6. Verify final remote sync state
326
+ 3. Run the \`sdd\` skill — it handles the full loop: open bugs, pending CRs,
327
+ documentation sync, code implementation, mark-synced, and commit.
328
+
329
+ 4. Push:
315
330
 
316
331
  \`\`\`bash
317
- sdd remote status
332
+ sdd push
318
333
  \`\`\`
319
334
 
320
335
  ## Rules
321
336
 
322
337
  1. Always check remote configuration before pull/push (\`sdd remote status\`)
323
- 2. Do not use \`sdd push --all\` unless the user explicitly asks for a full reseed
324
- 3. If pull reports conflicts, do not overwrite local files blindly; report conflicts and ask how to proceed
325
- 4. Do not edit files inside \`.sdd/\` manually
326
- 5. Keep status transitions explicit: enrich first, then \`sdd mark-drafts-enriched\`, then push
338
+ 3. Do not use \`sdd push --all\` unless the user explicitly asks for a full reseed
339
+ 4. If pull reports conflicts, do not overwrite local files blindly; report conflicts and ask how to proceed
340
+ 5. Do not edit files inside \`.sdd/\` manually
341
+ 6. Keep status transitions explicit: enrich first, then \`sdd mark-drafts-enriched\`, then push
342
+ 7. **Always commit before pushing** when the sync workflow makes code changes
327
343
 
328
344
  ## Related commands
329
345
 
@@ -332,6 +348,8 @@ sdd remote status
332
348
  - \`sdd pull\`
333
349
  - \`sdd drafts\`
334
350
  - \`sdd mark-drafts-enriched\`
351
+ - \`sdd sync\`
352
+ - \`sdd mark-synced\`
335
353
  - \`sdd push\`
336
354
  `;
337
355