spets 0.1.8 → 0.1.10

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.
@@ -10,6 +10,9 @@ import { parse as parseYaml } from "yaml";
10
10
  var SPETS_DIR = ".spets";
11
11
  var CONFIG_FILE = "config.yml";
12
12
  var STEPS_DIR = "steps";
13
+ var configCache = null;
14
+ var configCachePath = null;
15
+ var stepDefinitionCache = /* @__PURE__ */ new Map();
13
16
  function getSpetsDir(cwd = process.cwd()) {
14
17
  return join(cwd, SPETS_DIR);
15
18
  }
@@ -27,6 +30,9 @@ function spetsExists(cwd = process.cwd()) {
27
30
  }
28
31
  function loadConfig(cwd = process.cwd()) {
29
32
  const configPath = getConfigPath(cwd);
33
+ if (configCache && configCachePath === configPath) {
34
+ return configCache;
35
+ }
30
36
  if (!existsSync(configPath)) {
31
37
  throw new Error(`Spets config not found. Run 'spets init' first.`);
32
38
  }
@@ -35,9 +41,15 @@ function loadConfig(cwd = process.cwd()) {
35
41
  if (!config.steps || config.steps.length === 0) {
36
42
  throw new Error("Config must define at least one step");
37
43
  }
44
+ configCache = config;
45
+ configCachePath = configPath;
38
46
  return config;
39
47
  }
40
48
  function loadStepDefinition(stepName, cwd = process.cwd()) {
49
+ const cacheKey = `${cwd}:${stepName}`;
50
+ if (stepDefinitionCache.has(cacheKey)) {
51
+ return stepDefinitionCache.get(cacheKey);
52
+ }
41
53
  const stepDir = join(getStepsDir(cwd), stepName);
42
54
  const instructionPath = join(stepDir, "instruction.md");
43
55
  const templatePath = join(stepDir, "template.md");
@@ -46,19 +58,58 @@ function loadStepDefinition(stepName, cwd = process.cwd()) {
46
58
  }
47
59
  const instruction = readFileSync(instructionPath, "utf-8");
48
60
  const template = existsSync(templatePath) ? readFileSync(templatePath, "utf-8") : void 0;
49
- return {
61
+ const stepDef = {
50
62
  name: stepName,
51
63
  instruction,
52
64
  template
53
65
  };
66
+ stepDefinitionCache.set(cacheKey, stepDef);
67
+ return stepDef;
54
68
  }
55
69
  function getGitHubConfig(cwd = process.cwd()) {
56
70
  const config = loadConfig(cwd);
57
71
  return config.github;
58
72
  }
59
73
 
74
+ // src/core/slug.ts
75
+ function generateRandomSuffix() {
76
+ return Math.random().toString(36).substring(2, 6);
77
+ }
78
+ function generateDatePrefix() {
79
+ const now = /* @__PURE__ */ new Date();
80
+ const year = now.getFullYear();
81
+ const month = String(now.getMonth() + 1).padStart(2, "0");
82
+ const day = String(now.getDate()).padStart(2, "0");
83
+ return `${year}-${month}-${day}`;
84
+ }
85
+ function sanitizeDescription(description) {
86
+ let sanitized = description.toLowerCase();
87
+ sanitized = sanitized.replace(/[^\x00-\x7F]/g, "");
88
+ sanitized = sanitized.replace(/[^a-z0-9\s]/g, "");
89
+ sanitized = sanitized.trim();
90
+ sanitized = sanitized.replace(/\s+/g, " ");
91
+ sanitized = sanitized.replace(/\s+/g, "-");
92
+ if (sanitized.length > 10) {
93
+ sanitized = sanitized.substring(0, 10);
94
+ }
95
+ sanitized = sanitized.replace(/-+$/, "");
96
+ if (sanitized === "") {
97
+ sanitized = "task";
98
+ }
99
+ return sanitized;
100
+ }
101
+ function generateSlug(description) {
102
+ const datePrefix = generateDatePrefix();
103
+ const meaningfulName = sanitizeDescription(description);
104
+ const randomSuffix = generateRandomSuffix();
105
+ return `${datePrefix}-${meaningfulName}-${randomSuffix}`;
106
+ }
107
+
60
108
  // src/core/state.ts
61
- function generateTaskId() {
109
+ function generateTaskId(description) {
110
+ if (description) {
111
+ return generateSlug(description);
112
+ }
62
113
  const timestamp = Date.now().toString(36);
63
114
  const random = Math.random().toString(36).substring(2, 6);
64
115
  return `${timestamp}-${random}`;
@@ -132,11 +183,68 @@ function listTasks(cwd = process.cwd()) {
132
183
  }
133
184
  return readdirSync(outputsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
134
185
  }
186
+ function getStateCachePath(taskId, cwd = process.cwd()) {
187
+ return join2(getTaskDir(taskId, cwd), ".state-cache.json");
188
+ }
189
+ function loadStateCache(taskId, cwd = process.cwd()) {
190
+ const cachePath = getStateCachePath(taskId, cwd);
191
+ if (!existsSync2(cachePath)) {
192
+ return null;
193
+ }
194
+ try {
195
+ const cached = JSON.parse(readFileSync2(cachePath, "utf-8"));
196
+ return cached;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ function saveStateCache(taskId, state, cwd = process.cwd()) {
202
+ const cachePath = getStateCachePath(taskId, cwd);
203
+ const stepStatuses = {};
204
+ for (const [stepName, output] of state.outputs.entries()) {
205
+ stepStatuses[stepName] = output.status;
206
+ }
207
+ const cached = {
208
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
209
+ taskId: state.taskId,
210
+ userQuery: state.userQuery,
211
+ currentStepIndex: state.currentStepIndex,
212
+ currentStepName: state.currentStepName,
213
+ status: state.status,
214
+ stepStatuses
215
+ };
216
+ try {
217
+ writeFileSync(cachePath, JSON.stringify(cached, null, 2));
218
+ } catch {
219
+ }
220
+ }
221
+ function isStateCacheValid(cached, maxAgeMs = 5e3) {
222
+ const age = Date.now() - new Date(cached.lastUpdated).getTime();
223
+ return age < maxAgeMs;
224
+ }
135
225
  function getWorkflowState(taskId, config, cwd = process.cwd()) {
136
226
  const taskDir = getTaskDir(taskId, cwd);
137
227
  if (!existsSync2(taskDir)) {
138
228
  return null;
139
229
  }
230
+ const cached = loadStateCache(taskId, cwd);
231
+ if (cached && isStateCacheValid(cached)) {
232
+ const outputs2 = /* @__PURE__ */ new Map();
233
+ for (const [stepName, status] of Object.entries(cached.stepStatuses)) {
234
+ outputs2.set(stepName, {
235
+ path: getOutputPath(taskId, stepName, cwd),
236
+ status
237
+ });
238
+ }
239
+ return {
240
+ taskId: cached.taskId,
241
+ userQuery: cached.userQuery,
242
+ currentStepIndex: cached.currentStepIndex,
243
+ currentStepName: cached.currentStepName,
244
+ status: cached.status,
245
+ outputs: outputs2
246
+ };
247
+ }
140
248
  const outputs = /* @__PURE__ */ new Map();
141
249
  let lastApprovedIndex = -1;
142
250
  let currentStatus = "in_progress";
@@ -171,7 +279,7 @@ function getWorkflowState(taskId, config, cwd = process.cwd()) {
171
279
  if (currentDoc?.frontmatter.status === "draft" && currentDoc.frontmatter.open_questions?.length) {
172
280
  currentStatus = "paused";
173
281
  }
174
- return {
282
+ const state = {
175
283
  taskId,
176
284
  userQuery,
177
285
  currentStepIndex,
@@ -179,6 +287,8 @@ function getWorkflowState(taskId, config, cwd = process.cwd()) {
179
287
  status: currentStatus,
180
288
  outputs
181
289
  };
290
+ saveStateCache(taskId, state, cwd);
291
+ return state;
182
292
  }
183
293
  function saveTaskMetadata(taskId, userQuery, cwd = process.cwd()) {
184
294
  const taskDir = ensureTaskDir(taskId, cwd);
@@ -192,6 +302,21 @@ function loadTaskMetadata(taskId, cwd = process.cwd()) {
192
302
  }
193
303
  return JSON.parse(readFileSync2(metaPath, "utf-8"));
194
304
  }
305
+ function extractPlanSummary(taskId, cwd = process.cwd()) {
306
+ const doc = loadDocument(taskId, "01-plan", cwd);
307
+ if (!doc) {
308
+ return null;
309
+ }
310
+ const summaryMatch = doc.content.match(/## Summary\n\n(.+?)(?=\n\n##|\n\n$|$)/s);
311
+ if (summaryMatch) {
312
+ return summaryMatch[1].trim();
313
+ }
314
+ const goalMatch = doc.content.match(/## Goal\n\n(.+?)(?=\n\n##|\n\n$|$)/s);
315
+ if (goalMatch) {
316
+ return goalMatch[1].trim();
317
+ }
318
+ return null;
319
+ }
195
320
 
196
321
  export {
197
322
  getSpetsDir,
@@ -213,5 +338,6 @@ export {
213
338
  listTasks,
214
339
  getWorkflowState,
215
340
  saveTaskMetadata,
216
- loadTaskMetadata
341
+ loadTaskMetadata,
342
+ extractPlanSummary
217
343
  };
package/dist/index.js CHANGED
@@ -16,13 +16,13 @@ import {
16
16
  saveTaskMetadata,
17
17
  spetsExists,
18
18
  updateDocumentStatus
19
- } from "./chunk-YK5ZZE4P.js";
19
+ } from "./chunk-NOS3N4GT.js";
20
20
 
21
21
  // src/index.ts
22
22
  import { Command } from "commander";
23
23
 
24
24
  // src/commands/init.ts
25
- import { mkdirSync, writeFileSync } from "fs";
25
+ import { mkdirSync, writeFileSync, readFileSync } from "fs";
26
26
  import { join, dirname } from "path";
27
27
  import { fileURLToPath } from "url";
28
28
  import { execSync } from "child_process";
@@ -121,155 +121,20 @@ function createDefaultSteps(spetsDir) {
121
121
  writeFileSync(join(implementDir, "template.md"), getImplementTemplate());
122
122
  }
123
123
  function getPlanInstruction() {
124
- return `# Plan Step
125
-
126
- You are creating a technical plan for the given task.
127
-
128
- ## Your Goal
129
-
130
- Analyze the user's request and create a detailed implementation plan.
131
-
132
- ## Process
133
-
134
- 1. **Understand the Request**
135
- - Parse the user's query to identify the core requirements
136
- - Identify any ambiguities or missing information
137
-
138
- 2. **Ask Clarifying Questions** (if needed)
139
- - If requirements are unclear, list questions in the \`open_questions\` section
140
- - Questions should be specific and actionable
141
-
142
- 3. **Create the Plan**
143
- - Break down the task into concrete steps
144
- - Identify files to create/modify
145
- - Consider edge cases and potential issues
146
-
147
- ## Output Format
148
-
149
- Follow the template provided. Include:
150
- - Summary of what will be built
151
- - Step-by-step implementation plan
152
- - Files to be created/modified
153
- - Any open questions (if requirements are unclear)
154
- `;
124
+ const fullTemplate = readFileSync(join(__dirname, "..", ".spets", "steps", "01-plan", "instruction.md"), "utf-8");
125
+ return fullTemplate;
155
126
  }
156
127
  function getPlanTemplate() {
157
- return `# Plan: {{title}}
158
-
159
- ## Summary
160
-
161
- Brief description of what will be implemented.
162
-
163
- ## Requirements
164
-
165
- - Requirement 1
166
- - Requirement 2
167
-
168
- ## Implementation Steps
169
-
170
- ### Step 1: [Description]
171
-
172
- Details...
173
-
174
- ### Step 2: [Description]
175
-
176
- Details...
177
-
178
- ## Files to Modify
179
-
180
- | File | Action | Description |
181
- |------|--------|-------------|
182
- | path/to/file | Create/Modify | What changes |
183
-
184
- ## Open Questions
185
-
186
- <!-- Remove this section if no questions -->
187
-
188
- - Question 1?
189
- - Question 2?
190
-
191
- ## Risks & Considerations
192
-
193
- - Risk 1
194
- - Risk 2
195
- `;
128
+ const fullTemplate = readFileSync(join(__dirname, "..", ".spets", "steps", "01-plan", "template.md"), "utf-8");
129
+ return fullTemplate;
196
130
  }
197
131
  function getImplementInstruction() {
198
- return `# Implement Step
199
-
200
- You are implementing the plan from the previous step.
201
-
202
- ## Your Goal
203
-
204
- Write the actual code based on the approved plan.
205
-
206
- ## Process
207
-
208
- 1. **Review the Plan**
209
- - Read the approved plan document carefully
210
- - Understand all requirements and steps
211
-
212
- 2. **Implement**
213
- - Follow the plan step by step
214
- - Write clean, well-documented code
215
- - Handle edge cases identified in the plan
216
-
217
- 3. **Document Changes**
218
- - List all files created/modified
219
- - Explain key decisions made during implementation
220
-
221
- ## Output Format
222
-
223
- Follow the template provided. Include:
224
- - Summary of implementation
225
- - List of all changes made
226
- - Any deviations from the plan (with justification)
227
- - Testing notes
228
- `;
132
+ const fullTemplate = readFileSync(join(__dirname, "..", ".spets", "steps", "02-implement", "instruction.md"), "utf-8");
133
+ return fullTemplate;
229
134
  }
230
135
  function getImplementTemplate() {
231
- return `# Implementation: {{title}}
232
-
233
- ## Summary
234
-
235
- Brief description of what was implemented.
236
-
237
- ## Changes Made
238
-
239
- ### New Files
240
-
241
- | File | Description |
242
- |------|-------------|
243
- | path/to/file | What it does |
244
-
245
- ### Modified Files
246
-
247
- | File | Changes |
248
- |------|---------|
249
- | path/to/file | What changed |
250
-
251
- ## Key Decisions
252
-
253
- - Decision 1: Explanation
254
- - Decision 2: Explanation
255
-
256
- ## Deviations from Plan
257
-
258
- <!-- Remove if no deviations -->
259
-
260
- None / List any deviations with justification.
261
-
262
- ## Testing
263
-
264
- - [ ] Manual testing completed
265
- - [ ] Unit tests added
266
- - [ ] Integration tests pass
267
-
268
- ## Next Steps
269
-
270
- - Follow-up task 1
271
- - Follow-up task 2
272
- `;
136
+ const fullTemplate = readFileSync(join(__dirname, "..", ".spets", "steps", "02-implement", "template.md"), "utf-8");
137
+ return fullTemplate;
273
138
  }
274
139
  function createClaudeCommand(cwd) {
275
140
  const commandDir = join(cwd, ".claude", "commands");
@@ -458,7 +323,13 @@ jobs:
458
323
  - name: Checkout
459
324
  uses: actions/checkout@v4
460
325
  with:
461
- fetch-depth: 0
326
+ fetch-depth: 1
327
+ persist-credentials: false
328
+
329
+ - name: Setup Git
330
+ run: |
331
+ git config user.name "github-actions[bot]"
332
+ git config user.email "github-actions[bot]@users.noreply.github.com"
462
333
 
463
334
  - name: Parse Issue body
464
335
  id: parse
@@ -479,17 +350,33 @@ jobs:
479
350
 
480
351
  - name: Create and checkout branch
481
352
  run: |
353
+ git remote set-url origin https://x-access-token:${gh("secrets.PAT_TOKEN")}@github.com/${gh("github.repository")}.git
482
354
  git checkout -b ${gh("steps.parse.outputs.branch")}
483
355
  git push -u origin ${gh("steps.parse.outputs.branch")}
484
- env:
485
- GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
486
356
 
487
357
  - name: Setup Node.js
488
358
  uses: actions/setup-node@v4
489
359
  with:
490
360
  node-version: '20'
361
+ cache: 'npm'
362
+
363
+ - name: Cache global npm packages
364
+ uses: actions/cache@v4
365
+ with:
366
+ path: ~/.npm
367
+ key: ${gh("runner.os")}-npm-global-${gh("hashFiles('package-lock.json')")}
368
+ restore-keys: |
369
+ ${gh("runner.os")}-npm-global-
370
+
371
+ - name: Cache Claude Code
372
+ id: cache-claude
373
+ uses: actions/cache@v4
374
+ with:
375
+ path: /usr/local/lib/node_modules/@anthropic-ai/claude-code
376
+ key: claude-code-${gh("runner.os")}-v1
491
377
 
492
378
  - name: Install Claude Code
379
+ if: steps.cache-claude.outputs.cache-hit != 'true'
493
380
  run: npm install -g @anthropic-ai/claude-code
494
381
 
495
382
  - name: Install dependencies
@@ -505,13 +392,10 @@ jobs:
505
392
 
506
393
  - name: Push changes
507
394
  run: |
508
- git config user.name "github-actions[bot]"
509
- git config user.email "github-actions[bot]@users.noreply.github.com"
395
+ git remote set-url origin https://x-access-token:${gh("secrets.PAT_TOKEN")}@github.com/${gh("github.repository")}.git
510
396
  git add -A
511
397
  git diff --staged --quiet || git commit -m "Spets: Start workflow for #${gh("github.event.issue.number")}"
512
398
  git push
513
- env:
514
- GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
515
399
 
516
400
  # Handle commands from Issue/PR comments
517
401
  handle-command:
@@ -524,38 +408,108 @@ jobs:
524
408
  runs-on: ubuntu-latest
525
409
 
526
410
  steps:
527
- - name: Find linked branch
411
+ - name: Find branch from Issue or PR
528
412
  id: branch
413
+ env:
414
+ GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
529
415
  run: |
530
- # Try to find branch linked to this issue
531
- BRANCH=$(gh api repos/${gh("github.repository")}/issues/${gh("github.event.issue.number")} --jq '.body' | grep -oP 'spets/\\d+' || echo "")
532
- if [ -z "$BRANCH" ]; then
533
- BRANCH="spets/${gh("github.event.issue.number")}"
416
+ # Check if this is a PR (has pull_request field)
417
+ PR_BRANCH=$(gh api repos/${gh("github.repository")}/issues/${gh("github.event.issue.number")} --jq '.pull_request.url // empty' 2>/dev/null)
418
+
419
+ if [ -n "$PR_BRANCH" ]; then
420
+ # It's a PR - get head branch directly
421
+ BRANCH=$(gh api repos/${gh("github.repository")}/pulls/${gh("github.event.issue.number")} --jq '.head.ref')
422
+ echo "Found PR head branch: $BRANCH"
423
+ else
424
+ # It's an Issue - try to parse branch name from body
425
+ ISSUE_BODY=$(gh api repos/${gh("github.repository")}/issues/${gh("github.event.issue.number")} --jq '.body')
426
+ CUSTOM_BRANCH=$(echo "$ISSUE_BODY" | sed -n '/### Branch Name/,/###/{/###/!p;}' | sed '/^$/d' | head -1)
427
+
428
+ if [ -n "$CUSTOM_BRANCH" ]; then
429
+ BRANCH="$CUSTOM_BRANCH"
430
+ else
431
+ BRANCH="spets/${gh("github.event.issue.number")}"
432
+ fi
433
+ fi
434
+
435
+ echo "Checking for branch: $BRANCH"
436
+
437
+ # Check if branch exists on remote using gh api
438
+ if gh api "repos/${gh("github.repository")}/branches/$BRANCH" --silent 2>/dev/null; then
439
+ echo "name=$BRANCH" >> $GITHUB_OUTPUT
440
+ echo "exists=true" >> $GITHUB_OUTPUT
441
+ echo "Branch $BRANCH found!"
442
+ else
443
+ echo "exists=false" >> $GITHUB_OUTPUT
444
+ echo "expected=$BRANCH" >> $GITHUB_OUTPUT
445
+ echo "::error::Branch $BRANCH not found. Start workflow first by creating an Issue with 'spets' label."
534
446
  fi
535
- echo "name=$BRANCH" >> $GITHUB_OUTPUT
447
+
448
+ - name: Post error comment
449
+ if: steps.branch.outputs.exists == 'false'
450
+ run: |
451
+ gh issue comment ${gh("github.event.issue.number")} \\
452
+ -R "${gh("github.repository")}" \\
453
+ --body "\u274C **Spets Error**: Branch \\\`${gh("steps.branch.outputs.expected")}\\\` not found.
454
+
455
+ Please make sure the workflow was started properly. You can:
456
+ 1. Add the \\\`spets\\\` label to this issue to trigger the start workflow
457
+ 2. Or manually create the branch and run \\\`spets start\\\`"
536
458
  env:
537
459
  GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
538
460
 
461
+ - name: Exit if branch not found
462
+ if: steps.branch.outputs.exists == 'false'
463
+ run: exit 1
464
+
539
465
  - name: Checkout
540
466
  uses: actions/checkout@v4
541
467
  with:
542
468
  ref: ${gh("steps.branch.outputs.name")}
543
- fetch-depth: 0
469
+ fetch-depth: 1
470
+ persist-credentials: false
471
+
472
+ - name: Setup Git
473
+ run: |
474
+ git config user.name "github-actions[bot]"
475
+ git config user.email "github-actions[bot]@users.noreply.github.com"
544
476
 
545
477
  - name: Setup Node.js
546
478
  uses: actions/setup-node@v4
547
479
  with:
548
480
  node-version: '20'
481
+ cache: 'npm'
482
+
483
+ - name: Cache global npm packages
484
+ uses: actions/cache@v4
485
+ with:
486
+ path: ~/.npm
487
+ key: ${gh("runner.os")}-npm-global-${gh("hashFiles('package-lock.json')")}
488
+ restore-keys: |
489
+ ${gh("runner.os")}-npm-global-
490
+
491
+ - name: Cache Claude Code
492
+ id: cache-claude
493
+ uses: actions/cache@v4
494
+ with:
495
+ path: /usr/local/lib/node_modules/@anthropic-ai/claude-code
496
+ key: claude-code-${gh("runner.os")}-v1
549
497
 
550
498
  - name: Install Claude Code
499
+ if: steps.cache-claude.outputs.cache-hit != 'true'
551
500
  run: npm install -g @anthropic-ai/claude-code
552
501
 
553
502
  - name: Install dependencies
554
503
  run: npm ci
555
504
 
556
505
  - name: Run Spets command
506
+ id: spets
557
507
  run: |
558
508
  npx spets github --issue ${gh("github.event.issue.number")} --comment "$COMMENT"
509
+ # Check if PR should be created
510
+ if [[ "$COMMENT" == "/approve --pr"* ]]; then
511
+ echo "create_pr=true" >> $GITHUB_OUTPUT
512
+ fi
559
513
  env:
560
514
  COMMENT: ${gh("github.event.comment.body")}
561
515
  CLAUDE_CODE_OAUTH_TOKEN: ${gh("secrets.CLAUDE_CODE_OAUTH_TOKEN")}
@@ -563,13 +517,33 @@ jobs:
563
517
 
564
518
  - name: Push changes
565
519
  run: |
566
- git config user.name "github-actions[bot]"
567
- git config user.email "github-actions[bot]@users.noreply.github.com"
520
+ git remote set-url origin https://x-access-token:${gh("secrets.PAT_TOKEN")}@github.com/${gh("github.repository")}.git
568
521
  git add -A
569
522
  git diff --staged --quiet || git commit -m "Spets: Update from #${gh("github.event.issue.number")}"
570
523
  git push
524
+
525
+ - name: Create PR
526
+ if: steps.spets.outputs.create_pr == 'true'
527
+ run: |
528
+ PR_BODY="Closes #${gh("github.event.issue.number")}
529
+
530
+ ---
531
+
532
+ ## Spets Commands
533
+
534
+ | Command | Description |
535
+ |---------|-------------|
536
+ | \\\`/approve\\\` | Approve current step and continue |
537
+ | \\\`/approve --pr\\\` | Approve and create PR |
538
+ | \\\`/revise <feedback>\\\` | Request changes with feedback |
539
+ | \\\`/reject\\\` | Reject and stop workflow |"
540
+
541
+ gh pr create \\
542
+ --title "Spets: Issue #${gh("github.event.issue.number")}" \\
543
+ --body "$PR_BODY" \\
544
+ --repo ${gh("github.repository")}
571
545
  env:
572
- GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
546
+ GH_TOKEN: ${gh("secrets.PAT_TOKEN")}
573
547
  `;
574
548
  }
575
549
 
@@ -702,6 +676,16 @@ var Executor = class {
702
676
  this.config = options.config;
703
677
  this.cwd = options.cwd || process.cwd();
704
678
  }
679
+ /**
680
+ * Preload all step definitions into cache for better performance
681
+ */
682
+ async preloadSteps() {
683
+ await Promise.all(
684
+ this.config.steps.map(
685
+ (stepName) => Promise.resolve(loadStepDefinition(stepName, this.cwd))
686
+ )
687
+ );
688
+ }
705
689
  async executeWorkflow(taskId, userQuery, startIndex = 0, feedback) {
706
690
  let previousOutput;
707
691
  if (startIndex > 0) {
@@ -954,7 +938,6 @@ var CliPlatform = class extends BasePlatform {
954
938
  async callClaude(prompt) {
955
939
  return new Promise((resolve, reject) => {
956
940
  const proc = spawn2(this.claudeCommand, [
957
- "--print",
958
941
  "--permission-mode",
959
942
  "bypassPermissions"
960
943
  ], {
@@ -1024,6 +1007,21 @@ var CliPlatform = class extends BasePlatform {
1024
1007
 
1025
1008
  // src/platform/github.ts
1026
1009
  import { spawn as spawn3 } from "child_process";
1010
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelayMs = 1e3) {
1011
+ let lastError = null;
1012
+ for (let i = 0; i < maxRetries; i++) {
1013
+ try {
1014
+ return await fn();
1015
+ } catch (error) {
1016
+ lastError = error;
1017
+ if (i === maxRetries - 1) break;
1018
+ const delay = initialDelayMs * Math.pow(2, i);
1019
+ console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
1020
+ await new Promise((resolve) => setTimeout(resolve, delay));
1021
+ }
1022
+ }
1023
+ throw lastError || new Error("Retry failed");
1024
+ }
1027
1025
  var PauseForInputError = class extends Error {
1028
1026
  constructor(inputType, data) {
1029
1027
  super(`Paused waiting for ${inputType}`);
@@ -1211,26 +1209,39 @@ var GitHubPlatform = class extends BasePlatform {
1211
1209
  const { owner, repo, issueNumber } = this.config;
1212
1210
  await this.runGh(["issue", "comment", String(issueNumber), "--body", body, "-R", `${owner}/${repo}`]);
1213
1211
  }
1214
- async runGh(args) {
1215
- return new Promise((resolve, reject) => {
1216
- const proc = spawn3("gh", args, { stdio: ["pipe", "pipe", "pipe"] });
1217
- let stdout = "";
1218
- let stderr = "";
1219
- proc.stdout.on("data", (data) => {
1220
- stdout += data.toString();
1221
- });
1222
- proc.stderr.on("data", (data) => {
1223
- stderr += data.toString();
1224
- });
1225
- proc.on("close", (code) => {
1226
- if (code !== 0) {
1227
- reject(new Error(`gh command failed: ${stderr}`));
1228
- } else {
1229
- resolve(stdout);
1230
- }
1231
- });
1232
- proc.on("error", (err) => {
1233
- reject(new Error(`Failed to run gh: ${err.message}`));
1212
+ async runGh(args, timeoutMs = 3e4) {
1213
+ return retryWithBackoff(async () => {
1214
+ return new Promise((resolve, reject) => {
1215
+ const proc = spawn3("gh", args, { stdio: ["pipe", "pipe", "pipe"] });
1216
+ let stdout = "";
1217
+ let stderr = "";
1218
+ let isTimedOut = false;
1219
+ const timeoutId = setTimeout(() => {
1220
+ isTimedOut = true;
1221
+ proc.kill();
1222
+ reject(new Error(`gh command timed out after ${timeoutMs}ms`));
1223
+ }, timeoutMs);
1224
+ proc.stdout.on("data", (data) => {
1225
+ stdout += data.toString();
1226
+ });
1227
+ proc.stderr.on("data", (data) => {
1228
+ stderr += data.toString();
1229
+ });
1230
+ proc.on("close", (code) => {
1231
+ clearTimeout(timeoutId);
1232
+ if (isTimedOut) return;
1233
+ if (code !== 0) {
1234
+ reject(new Error(`gh command failed: ${stderr}`));
1235
+ } else {
1236
+ resolve(stdout);
1237
+ }
1238
+ });
1239
+ proc.on("error", (err) => {
1240
+ clearTimeout(timeoutId);
1241
+ if (!isTimedOut) {
1242
+ reject(new Error(`Failed to run gh: ${err.message}`));
1243
+ }
1244
+ });
1234
1245
  });
1235
1246
  });
1236
1247
  }
@@ -1264,11 +1275,9 @@ var GitHubPlatform = class extends BasePlatform {
1264
1275
  async callClaude(prompt) {
1265
1276
  return new Promise((resolve, reject) => {
1266
1277
  const proc = spawn3("claude", [
1267
- "-p",
1268
- prompt,
1269
1278
  "--permission-mode",
1270
1279
  "bypassPermissions"
1271
- ], { stdio: ["ignore", "pipe", "pipe"] });
1280
+ ], { stdio: ["pipe", "pipe", "pipe"] });
1272
1281
  let stdout = "";
1273
1282
  let stderr = "";
1274
1283
  proc.stdout.on("data", (data) => {
@@ -1277,6 +1286,8 @@ var GitHubPlatform = class extends BasePlatform {
1277
1286
  proc.stderr.on("data", (data) => {
1278
1287
  stderr += data.toString();
1279
1288
  });
1289
+ proc.stdin.write(prompt);
1290
+ proc.stdin.end();
1280
1291
  proc.on("close", (code) => {
1281
1292
  if (code !== 0) {
1282
1293
  reject(new Error(`Claude CLI failed (code ${code}): stderr=${stderr}, stdout=${stdout}`));
@@ -1402,7 +1413,7 @@ async function startCommand(query, options) {
1402
1413
  process.exit(1);
1403
1414
  }
1404
1415
  const config = loadConfig(cwd);
1405
- const taskId = generateTaskId();
1416
+ const taskId = generateTaskId(query);
1406
1417
  saveTaskMetadata(taskId, query, cwd);
1407
1418
  let platform;
1408
1419
  if (options.github || options.issue !== void 0 || options.pr !== void 0) {
@@ -1612,28 +1623,28 @@ function installClaudePlugin() {
1612
1623
  const claudeDir = join3(homedir(), ".claude");
1613
1624
  const commandsDir = join3(claudeDir, "commands");
1614
1625
  mkdirSync2(commandsDir, { recursive: true });
1615
- const skillPath = join3(commandsDir, "sdd-do.md");
1626
+ const skillPath = join3(commandsDir, "spets.md");
1616
1627
  writeFileSync2(skillPath, getClaudeSkillContent());
1617
1628
  console.log("Installed Claude Code plugin.");
1618
1629
  console.log(`Location: ${skillPath}`);
1619
1630
  console.log("");
1620
1631
  console.log("Usage in Claude Code:");
1621
- console.log(' /sdd-do "your task description"');
1632
+ console.log(' /spets "your task description"');
1622
1633
  console.log("");
1623
1634
  console.log("This skill runs deterministically within your Claude Code session.");
1624
1635
  console.log("No additional Claude processes are spawned.");
1625
1636
  }
1626
1637
  async function uninstallPlugin(name) {
1627
1638
  if (name === "claude") {
1628
- const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1629
- const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1639
+ const skillPath = join3(homedir(), ".claude", "commands", "spets.md");
1640
+ const legacySkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1630
1641
  let uninstalled = false;
1631
- if (existsSync3(oldSkillPath)) {
1632
- rmSync(oldSkillPath);
1642
+ if (existsSync3(skillPath)) {
1643
+ rmSync(skillPath);
1633
1644
  uninstalled = true;
1634
1645
  }
1635
- if (existsSync3(newSkillPath)) {
1636
- rmSync(newSkillPath);
1646
+ if (existsSync3(legacySkillPath)) {
1647
+ rmSync(legacySkillPath);
1637
1648
  uninstalled = true;
1638
1649
  }
1639
1650
  if (uninstalled) {
@@ -1649,11 +1660,11 @@ async function uninstallPlugin(name) {
1649
1660
  async function listPlugins() {
1650
1661
  console.log("Available plugins:");
1651
1662
  console.log("");
1652
- console.log(" claude - Claude Code /sdd-do skill");
1663
+ console.log(" claude - Claude Code /spets skill");
1653
1664
  console.log("");
1654
- const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1655
- const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1656
- const claudeInstalled = existsSync3(oldSkillPath) || existsSync3(newSkillPath);
1665
+ const skillPath = join3(homedir(), ".claude", "commands", "spets.md");
1666
+ const legacySkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1667
+ const claudeInstalled = existsSync3(skillPath) || existsSync3(legacySkillPath);
1657
1668
  console.log("Installed:");
1658
1669
  if (claudeInstalled) {
1659
1670
  console.log(" - claude");
@@ -1662,7 +1673,7 @@ async function listPlugins() {
1662
1673
  }
1663
1674
  }
1664
1675
  function getClaudeSkillContent() {
1665
- return `# SDD-Do Skill
1676
+ return `# Spets - Spec Driven Development
1666
1677
 
1667
1678
  Spec-Driven Development workflow execution skill for Claude Code.
1668
1679
 
@@ -1671,7 +1682,7 @@ Spec-Driven Development workflow execution skill for Claude Code.
1671
1682
  ## When to Use This Skill
1672
1683
 
1673
1684
  Automatically invoked when user uses:
1674
- - \`/sdd-do\` - Run SDD workflow
1685
+ - \`/spets\` - Run Spets workflow
1675
1686
 
1676
1687
  ---
1677
1688
 
@@ -1893,7 +1904,7 @@ async function githubCommand(options) {
1893
1904
  }
1894
1905
  if (!taskId) {
1895
1906
  const config2 = loadConfig(cwd);
1896
- const { listTasks: listTasks2 } = await import("./state-JLYPJWUT.js");
1907
+ const { listTasks: listTasks2 } = await import("./state-54IS3PZH.js");
1897
1908
  const tasks = listTasks2(cwd);
1898
1909
  for (const tid of tasks) {
1899
1910
  const state2 = getWorkflowState(tid, config2, cwd);
@@ -2072,7 +2083,7 @@ _Updated by [Spets](https://github.com/eatnug/spets)_`;
2072
2083
  }
2073
2084
 
2074
2085
  // src/orchestrator/index.ts
2075
- import { readFileSync, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
2086
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
2076
2087
  import { join as join4, dirname as dirname2 } from "path";
2077
2088
  import matter from "gray-matter";
2078
2089
  var Orchestrator = class {
@@ -2110,7 +2121,7 @@ var Orchestrator = class {
2110
2121
  if (!existsSync4(statePath)) {
2111
2122
  return null;
2112
2123
  }
2113
- const data = JSON.parse(readFileSync(statePath, "utf-8"));
2124
+ const data = JSON.parse(readFileSync2(statePath, "utf-8"));
2114
2125
  return data;
2115
2126
  }
2116
2127
  saveState(state) {
@@ -2129,7 +2140,7 @@ var Orchestrator = class {
2129
2140
  if (!existsSync4(specPath)) {
2130
2141
  return [];
2131
2142
  }
2132
- const content = readFileSync(specPath, "utf-8");
2143
+ const content = readFileSync2(specPath, "utf-8");
2133
2144
  const { data } = matter(content);
2134
2145
  const questions = [];
2135
2146
  if (data.open_questions && Array.isArray(data.open_questions)) {
@@ -2148,11 +2159,6 @@ var Orchestrator = class {
2148
2159
  }
2149
2160
  return questions;
2150
2161
  }
2151
- generateTaskId(description) {
2152
- const timestamp = Date.now().toString(36);
2153
- const random = Math.random().toString(36).substring(2, 6);
2154
- return `${timestamp}-${random}`;
2155
- }
2156
2162
  // ===========================================================================
2157
2163
  // Protocol Response Builders
2158
2164
  // ===========================================================================
@@ -2258,7 +2264,7 @@ var Orchestrator = class {
2258
2264
  cmdInit(description) {
2259
2265
  try {
2260
2266
  const steps = this.getSteps();
2261
- const taskId = this.generateTaskId(description);
2267
+ const taskId = generateTaskId(description);
2262
2268
  const state = {
2263
2269
  taskId,
2264
2270
  description,
@@ -2306,7 +2312,7 @@ var Orchestrator = class {
2306
2312
  }
2307
2313
  const specPath = this.getSpecPath(taskId, state.currentStep);
2308
2314
  if (existsSync4(specPath)) {
2309
- const content = readFileSync(specPath, "utf-8");
2315
+ const content = readFileSync2(specPath, "utf-8");
2310
2316
  const { content: body, data } = matter(content);
2311
2317
  if (data.open_questions && Array.isArray(data.open_questions)) {
2312
2318
  data.open_questions = data.open_questions.map((q, i) => ({
@@ -2333,7 +2339,7 @@ var Orchestrator = class {
2333
2339
  const steps = this.getSteps();
2334
2340
  const specPath = this.getSpecPath(taskId, state.currentStep);
2335
2341
  if (existsSync4(specPath)) {
2336
- const content = readFileSync(specPath, "utf-8");
2342
+ const content = readFileSync2(specPath, "utf-8");
2337
2343
  const { content: body, data } = matter(content);
2338
2344
  data.status = "approved";
2339
2345
  data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -2375,7 +2381,7 @@ var Orchestrator = class {
2375
2381
  }
2376
2382
  const specPath = this.getSpecPath(taskId, state.currentStep);
2377
2383
  if (existsSync4(specPath)) {
2378
- const content = readFileSync(specPath, "utf-8");
2384
+ const content = readFileSync2(specPath, "utf-8");
2379
2385
  const { content: body, data } = matter(content);
2380
2386
  data.status = "rejected";
2381
2387
  data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createDocument,
3
3
  ensureTaskDir,
4
+ extractPlanSummary,
4
5
  generateTaskId,
5
6
  getOutputPath,
6
7
  getTaskDir,
@@ -12,10 +13,11 @@ import {
12
13
  saveDocument,
13
14
  saveTaskMetadata,
14
15
  updateDocumentStatus
15
- } from "./chunk-YK5ZZE4P.js";
16
+ } from "./chunk-NOS3N4GT.js";
16
17
  export {
17
18
  createDocument,
18
19
  ensureTaskDir,
20
+ extractPlanSummary,
19
21
  generateTaskId,
20
22
  getOutputPath,
21
23
  getTaskDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spets",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Spec Driven Development Execution Framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",