@bradygaster/squad-sdk 0.8.17 → 0.8.19
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/adapter/client.d.ts.map +1 -1
- package/dist/adapter/client.js +2 -0
- package/dist/adapter/client.js.map +1 -1
- package/dist/config/init.d.ts +43 -2
- package/dist/config/init.d.ts.map +1 -1
- package/dist/config/init.js +389 -46
- package/dist/config/init.js.map +1 -1
- package/dist/index.d.ts +8 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -9
- package/dist/index.js.map +1 -1
- package/dist/ralph/index.js +5 -5
- package/dist/ralph/index.js.map +1 -1
- package/dist/remote/bridge.d.ts +2 -0
- package/dist/remote/bridge.d.ts.map +1 -1
- package/dist/remote/bridge.js +34 -4
- package/dist/remote/bridge.js.map +1 -1
- package/dist/resolution.d.ts +13 -0
- package/dist/resolution.d.ts.map +1 -1
- package/dist/resolution.js +9 -1
- package/dist/resolution.js.map +1 -1
- package/dist/sharing/consult.d.ts +226 -0
- package/dist/sharing/consult.d.ts.map +1 -0
- package/dist/sharing/consult.js +818 -0
- package/dist/sharing/consult.js.map +1 -0
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/package.json +207 -205
- package/templates/casting-history.json +4 -0
- package/templates/casting-policy.json +35 -0
- package/templates/casting-registry.json +3 -0
- package/templates/ceremonies.md +41 -0
- package/templates/charter.md +53 -0
- package/templates/constraint-tracking.md +38 -0
- package/templates/copilot-instructions.md +46 -0
- package/templates/history.md +10 -0
- package/templates/identity/now.md +9 -0
- package/templates/identity/wisdom.md +15 -0
- package/templates/mcp-config.md +98 -0
- package/templates/multi-agent-format.md +28 -0
- package/templates/orchestration-log.md +27 -0
- package/templates/plugin-marketplace.md +49 -0
- package/templates/raw-agent-output.md +37 -0
- package/templates/roster.md +60 -0
- package/templates/routing.md +54 -0
- package/templates/run-output.md +50 -0
- package/templates/scribe-charter.md +119 -0
- package/templates/skill.md +24 -0
- package/templates/skills/project-conventions/SKILL.md +56 -0
- package/templates/squad.agent.md +1146 -0
- package/templates/workflows/squad-ci.yml +24 -0
- package/templates/workflows/squad-docs.yml +50 -0
- package/templates/workflows/squad-heartbeat.yml +316 -0
- package/templates/workflows/squad-insider-release.yml +61 -0
- package/templates/workflows/squad-issue-assign.yml +161 -0
- package/templates/workflows/squad-label-enforce.yml +181 -0
- package/templates/workflows/squad-main-guard.yml +129 -0
- package/templates/workflows/squad-preview.yml +55 -0
- package/templates/workflows/squad-promote.yml +121 -0
- package/templates/workflows/squad-release.yml +77 -0
- package/templates/workflows/squad-triage.yml +260 -0
- package/templates/workflows/sync-squad-labels.yml +169 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consult mode SDK — setup, extraction, license detection, learning classification.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the complete SDK surface for consult mode:
|
|
5
|
+
*
|
|
6
|
+
* High-level operations (mirror CLI commands):
|
|
7
|
+
* - setupConsultMode(): Initialize consult mode in a project
|
|
8
|
+
* - extractLearnings(): Extract learnings from a consult session
|
|
9
|
+
*
|
|
10
|
+
* Low-level utilities (used by high-level operations):
|
|
11
|
+
* - detectLicense(): Identify project license type (permissive/copyleft/unknown)
|
|
12
|
+
* - logConsultation(): Write/append consultation log entries
|
|
13
|
+
* - mergeToPersonalSquad(): Merge generic learnings to personal squad
|
|
14
|
+
*
|
|
15
|
+
* @module sharing/consult
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import { resolveGlobalSquadPath } from '../resolution.js';
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Typed Errors
|
|
24
|
+
// ============================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when setupConsultMode is called but no personal squad exists.
|
|
27
|
+
* Consumers can catch this specifically to prompt users to run `squad init --global`.
|
|
28
|
+
*/
|
|
29
|
+
export class PersonalSquadNotFoundError extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('No personal squad found. Run `squad init --global` first to create your personal squad.');
|
|
32
|
+
this.name = 'PersonalSquadNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when extraction is disabled for a consult session.
|
|
37
|
+
* Consumers can catch this specifically to suggest using --force.
|
|
38
|
+
*/
|
|
39
|
+
export class ExtractionDisabledError extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super('Extraction is disabled for this consult session.\n' +
|
|
42
|
+
'This was configured in your personal squad settings.\n' +
|
|
43
|
+
'Use --force to override.');
|
|
44
|
+
this.name = 'ExtractionDisabledError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Consult Mode Agent File
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/**
|
|
51
|
+
* Consult mode preamble to inject after frontmatter in squad.agent.md.
|
|
52
|
+
* This tells Squad it's in consult mode and should skip Init Mode.
|
|
53
|
+
*/
|
|
54
|
+
const CONSULT_MODE_PREAMBLE = `
|
|
55
|
+
<!-- consult-mode: true -->
|
|
56
|
+
|
|
57
|
+
## ⚡ Consult Mode Active
|
|
58
|
+
|
|
59
|
+
This project is in **consult mode**. Your personal squad has been copied into \`.squad/\` for this session.
|
|
60
|
+
|
|
61
|
+
**Key differences from normal mode:**
|
|
62
|
+
- **Skip Init Mode** — The team already exists (copied from your personal squad)
|
|
63
|
+
- **Isolated changes** — All changes stay local until you run \`squad extract\`
|
|
64
|
+
- **Invisible to project** — Both \`.squad/\` and this agent file are in \`.git/info/exclude\`
|
|
65
|
+
|
|
66
|
+
**When done:** Run \`squad extract\` to review learnings and merge generic ones back to your personal squad.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
`;
|
|
71
|
+
/**
|
|
72
|
+
* Get the full squad.agent.md template path.
|
|
73
|
+
* Looks in the SDK package's templates directory.
|
|
74
|
+
*/
|
|
75
|
+
function getSquadAgentTemplatePath() {
|
|
76
|
+
// Use fileURLToPath for cross-platform compatibility (handles Windows drive letters, URL encoding)
|
|
77
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
78
|
+
// Try relative to this file (in dist/)
|
|
79
|
+
const distPath = path.resolve(currentDir, '../../templates/squad.agent.md');
|
|
80
|
+
if (fs.existsSync(distPath)) {
|
|
81
|
+
return distPath;
|
|
82
|
+
}
|
|
83
|
+
// Try relative to package root
|
|
84
|
+
const pkgPath = path.resolve(currentDir, '../../../templates/squad.agent.md');
|
|
85
|
+
if (fs.existsSync(pkgPath)) {
|
|
86
|
+
return pkgPath;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get the git remote URL for a repository.
|
|
92
|
+
* Converts SSH URLs to HTTPS format for display.
|
|
93
|
+
*/
|
|
94
|
+
function getGitRemoteUrl(projectRoot) {
|
|
95
|
+
try {
|
|
96
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
encoding: 'utf-8',
|
|
99
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
100
|
+
}).trim();
|
|
101
|
+
// Convert SSH URL to HTTPS for readability
|
|
102
|
+
// git@github.com:owner/repo.git → https://github.com/owner/repo
|
|
103
|
+
if (remoteUrl.startsWith('git@')) {
|
|
104
|
+
const match = remoteUrl.match(/git@([^:]+):(.+?)(\.git)?$/);
|
|
105
|
+
if (match) {
|
|
106
|
+
return `https://${match[1]}/${match[2]}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Remove .git suffix if present
|
|
110
|
+
return remoteUrl.replace(/\.git$/, '');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generate squad.agent.md for consult mode.
|
|
118
|
+
* Uses the full template with consult mode preamble injected.
|
|
119
|
+
*/
|
|
120
|
+
function getConsultAgentContent(projectName) {
|
|
121
|
+
const templatePath = getSquadAgentTemplatePath();
|
|
122
|
+
if (templatePath && fs.existsSync(templatePath)) {
|
|
123
|
+
const template = fs.readFileSync(templatePath, 'utf-8');
|
|
124
|
+
// Find the end of frontmatter (second ---)
|
|
125
|
+
const frontmatterEnd = template.indexOf('---', template.indexOf('---') + 3);
|
|
126
|
+
if (frontmatterEnd !== -1) {
|
|
127
|
+
const insertPoint = frontmatterEnd + 3;
|
|
128
|
+
const before = template.slice(0, insertPoint);
|
|
129
|
+
const after = template.slice(insertPoint);
|
|
130
|
+
// Update description in frontmatter for consult mode
|
|
131
|
+
const updatedBefore = before.replace(/description:\s*"[^"]*"/, `description: "Your AI team. Consulting on ${projectName} using your personal squad."`);
|
|
132
|
+
return updatedBefore + '\n' + CONSULT_MODE_PREAMBLE + after;
|
|
133
|
+
}
|
|
134
|
+
// Fallback: prepend preamble
|
|
135
|
+
return template + '\n' + CONSULT_MODE_PREAMBLE;
|
|
136
|
+
}
|
|
137
|
+
// Fallback: minimal agent if template not found
|
|
138
|
+
return `---
|
|
139
|
+
name: Squad
|
|
140
|
+
description: "Your AI team. Consulting on ${projectName} using your personal squad."
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
${CONSULT_MODE_PREAMBLE}
|
|
144
|
+
|
|
145
|
+
You are **Squad (Consultant)** — working on **${projectName}** using a copy of your personal squad.
|
|
146
|
+
|
|
147
|
+
### Available Context (local copy in .squad/)
|
|
148
|
+
|
|
149
|
+
- **Team:** \`.squad/team.md\` for roster and roles
|
|
150
|
+
- **Routing:** \`.squad/routing.md\` for task routing rules
|
|
151
|
+
- **Decisions:** \`.squad/decisions.md\` for your established patterns
|
|
152
|
+
- **Skills:** \`.squad/skills/\` for reusable capabilities
|
|
153
|
+
- **Agents:** \`.squad/agents/\` for your squad agents
|
|
154
|
+
|
|
155
|
+
Work as you would with your personal squad, but in this external codebase.
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Scribe Charter Patching for Consult Mode
|
|
160
|
+
// ============================================================================
|
|
161
|
+
/**
|
|
162
|
+
* Consult mode instructions to append to Scribe charter.
|
|
163
|
+
* This enables Scribe to classify decisions as generic or project-specific.
|
|
164
|
+
*/
|
|
165
|
+
const CONSULT_MODE_SCRIBE_PATCH = `
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Consult Mode Extraction
|
|
170
|
+
|
|
171
|
+
**This squad is in consult mode.** When merging decisions from the inbox, also classify each decision:
|
|
172
|
+
|
|
173
|
+
### Classification
|
|
174
|
+
|
|
175
|
+
For each decision in \`.squad/decisions/inbox/\`:
|
|
176
|
+
|
|
177
|
+
1. **Generic** (applies to any project) → Copy to \`.squad/extract/\` with the same filename
|
|
178
|
+
- Signals: "always use", "never use", "prefer X over Y", "best practice", coding standards, patterns that work anywhere
|
|
179
|
+
- These will be extracted to the personal squad via \`squad extract\`
|
|
180
|
+
|
|
181
|
+
2. **Project-specific** (only applies here) → Keep in local \`decisions.md\` only
|
|
182
|
+
- Signals: Contains file paths from this project, references "this project/codebase/repo", mentions project-specific config/APIs/schemas
|
|
183
|
+
|
|
184
|
+
Generic decisions go to BOTH \`.squad/decisions.md\` (for this session) AND \`.squad/extract/\` (for later extraction).
|
|
185
|
+
|
|
186
|
+
### Extract Directory
|
|
187
|
+
|
|
188
|
+
\`\`\`
|
|
189
|
+
.squad/extract/ # Generic learnings staged for personal squad
|
|
190
|
+
├── decision-1.md # Ready for extraction
|
|
191
|
+
└── pattern-auth.md # Ready for extraction
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
Run \`squad extract\` to review and merge these to your personal squad.
|
|
195
|
+
`;
|
|
196
|
+
/**
|
|
197
|
+
* Patch the Scribe charter in the copied squad with consult mode instructions.
|
|
198
|
+
*/
|
|
199
|
+
function patchScribeCharterForConsultMode(squadDir) {
|
|
200
|
+
const charterPath = path.join(squadDir, 'agents', 'scribe', 'charter.md');
|
|
201
|
+
if (!fs.existsSync(charterPath)) {
|
|
202
|
+
// No scribe charter to patch — skip silently
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const existing = fs.readFileSync(charterPath, 'utf-8');
|
|
206
|
+
// Don't patch if already patched
|
|
207
|
+
if (existing.includes('Consult Mode Extraction')) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
fs.appendFileSync(charterPath, CONSULT_MODE_SCRIBE_PATCH);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* List files recursively in a directory.
|
|
214
|
+
*/
|
|
215
|
+
function listFilesInDir(dir, basePath = '') {
|
|
216
|
+
if (!fs.existsSync(dir))
|
|
217
|
+
return [];
|
|
218
|
+
const files = [];
|
|
219
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
files.push(...listFilesInDir(path.join(dir, entry.name), relativePath));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
files.push(relativePath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return files;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get the personal squad root path.
|
|
233
|
+
* Returns {globalSquadPath}/.squad/
|
|
234
|
+
*/
|
|
235
|
+
export function getPersonalSquadRoot() {
|
|
236
|
+
return path.resolve(resolveGlobalSquadPath(), '.squad');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the git exclude path using git rev-parse (handles worktrees/submodules).
|
|
240
|
+
*
|
|
241
|
+
* @param cwd - Working directory inside the git repo
|
|
242
|
+
* @throws Error if not a git repository
|
|
243
|
+
*/
|
|
244
|
+
export function resolveGitExcludePath(cwd) {
|
|
245
|
+
try {
|
|
246
|
+
return execSync('git rev-parse --git-path info/exclude', {
|
|
247
|
+
cwd,
|
|
248
|
+
encoding: 'utf-8',
|
|
249
|
+
}).trim();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
throw new Error('Not a git repository. Consult mode requires git.');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Set up consult mode in a project.
|
|
257
|
+
*
|
|
258
|
+
* Creates .squad/ with consult: true, pointing to your personal squad.
|
|
259
|
+
* Creates .github/agents/squad.agent.md for `gh copilot --agent squad` support.
|
|
260
|
+
* Both are hidden via .git/info/exclude (never committed).
|
|
261
|
+
*
|
|
262
|
+
* @param options - Setup options
|
|
263
|
+
* @returns Setup result with paths and metadata
|
|
264
|
+
* @throws Error if not a git repo, personal squad missing, or already squadified
|
|
265
|
+
*/
|
|
266
|
+
export async function setupConsultMode(options = {}) {
|
|
267
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
268
|
+
const personalSquadRoot = options.personalSquadRoot || getPersonalSquadRoot();
|
|
269
|
+
const dryRun = options.dryRun ?? false;
|
|
270
|
+
const squadDir = path.resolve(projectRoot, '.squad');
|
|
271
|
+
const projectName = options.projectName || path.basename(projectRoot);
|
|
272
|
+
const agentFile = path.resolve(projectRoot, '.github', 'agents', 'squad.agent.md');
|
|
273
|
+
// Check if we're in a git repository (handle worktrees/submodules where .git is a file)
|
|
274
|
+
const gitPath = path.resolve(projectRoot, '.git');
|
|
275
|
+
if (!fs.existsSync(gitPath)) {
|
|
276
|
+
throw new Error('Not a git repository. Consult mode requires git.');
|
|
277
|
+
}
|
|
278
|
+
// Resolve exclude path via git rev-parse (handles worktrees/submodules)
|
|
279
|
+
// Normalize to absolute path in case it's relative
|
|
280
|
+
const gitExclude = (() => {
|
|
281
|
+
const excludePath = resolveGitExcludePath(projectRoot);
|
|
282
|
+
return path.isAbsolute(excludePath) ? excludePath : path.resolve(projectRoot, excludePath);
|
|
283
|
+
})();
|
|
284
|
+
// Check if personal squad exists
|
|
285
|
+
if (!fs.existsSync(personalSquadRoot)) {
|
|
286
|
+
throw new PersonalSquadNotFoundError();
|
|
287
|
+
}
|
|
288
|
+
// Read source squad's config to inherit extractionDisabled setting
|
|
289
|
+
// Option takes precedence, then fall back to source config
|
|
290
|
+
let extractionDisabled = options.extractionDisabled ?? false;
|
|
291
|
+
const sourceConfigPath = path.join(personalSquadRoot, 'config.json');
|
|
292
|
+
if (fs.existsSync(sourceConfigPath)) {
|
|
293
|
+
try {
|
|
294
|
+
const sourceConfig = JSON.parse(fs.readFileSync(sourceConfigPath, 'utf-8'));
|
|
295
|
+
// Inherit from source unless explicitly overridden in options
|
|
296
|
+
if (options.extractionDisabled === undefined && sourceConfig.extractionDisabled) {
|
|
297
|
+
extractionDisabled = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Ignore malformed config
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Check if project already has .squad/
|
|
305
|
+
if (fs.existsSync(squadDir)) {
|
|
306
|
+
throw new Error('This project already has a .squad/ directory. Cannot use consult mode on squadified projects.');
|
|
307
|
+
}
|
|
308
|
+
// List files in personal squad (for dry run preview or later count)
|
|
309
|
+
const sourceFiles = listFilesInDir(personalSquadRoot);
|
|
310
|
+
if (!dryRun) {
|
|
311
|
+
// Copy personal squad contents into project's .squad/
|
|
312
|
+
// This isolates changes during the consult session
|
|
313
|
+
fs.cpSync(personalSquadRoot, squadDir, { recursive: true });
|
|
314
|
+
// Write/overwrite config.json with consult: true
|
|
315
|
+
// Include SquadDirConfig fields so loadDirConfig() can read it
|
|
316
|
+
// Note: version must be numeric for loadDirConfig() compatibility
|
|
317
|
+
const config = {
|
|
318
|
+
version: 1,
|
|
319
|
+
teamRoot: personalSquadRoot,
|
|
320
|
+
consult: true,
|
|
321
|
+
sourceSquad: personalSquadRoot,
|
|
322
|
+
projectName,
|
|
323
|
+
createdAt: new Date().toISOString(),
|
|
324
|
+
extractionDisabled,
|
|
325
|
+
};
|
|
326
|
+
fs.writeFileSync(path.join(squadDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
327
|
+
// Create sessions directory for tracking (if not copied)
|
|
328
|
+
const sessionsDir = path.join(squadDir, 'sessions');
|
|
329
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
330
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
// Create extract/ directory for staging generic learnings
|
|
333
|
+
const extractDir = path.join(squadDir, 'extract');
|
|
334
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
335
|
+
// Patch scribe-charter.md with consult mode extraction instructions
|
|
336
|
+
patchScribeCharterForConsultMode(squadDir);
|
|
337
|
+
// Create .github/agents/squad.agent.md for `gh copilot --agent squad`
|
|
338
|
+
const agentDir = path.dirname(agentFile);
|
|
339
|
+
if (!fs.existsSync(agentDir)) {
|
|
340
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
fs.writeFileSync(agentFile, getConsultAgentContent(projectName), 'utf-8');
|
|
343
|
+
// Add .squad/ and .github/agents/squad.agent.md to .git/info/exclude
|
|
344
|
+
const excludeDir = path.dirname(gitExclude);
|
|
345
|
+
if (!fs.existsSync(excludeDir)) {
|
|
346
|
+
fs.mkdirSync(excludeDir, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
const excludeContent = fs.existsSync(gitExclude)
|
|
349
|
+
? fs.readFileSync(gitExclude, 'utf-8')
|
|
350
|
+
: '';
|
|
351
|
+
const excludeLines = [];
|
|
352
|
+
if (!excludeContent.includes('.squad/')) {
|
|
353
|
+
excludeLines.push('.squad/');
|
|
354
|
+
}
|
|
355
|
+
if (!excludeContent.includes('.github/agents/squad.agent.md')) {
|
|
356
|
+
excludeLines.push('.github/agents/squad.agent.md');
|
|
357
|
+
}
|
|
358
|
+
if (excludeLines.length > 0) {
|
|
359
|
+
fs.appendFileSync(gitExclude, '\n# Squad consult mode (local only)\n' + excludeLines.join('\n') + '\n');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// List files created (from squad dir after copy, or from source for dry run)
|
|
363
|
+
const createdFiles = dryRun ? sourceFiles : listFilesInDir(squadDir);
|
|
364
|
+
return {
|
|
365
|
+
squadDir,
|
|
366
|
+
personalSquadRoot,
|
|
367
|
+
gitExclude,
|
|
368
|
+
projectName,
|
|
369
|
+
dryRun,
|
|
370
|
+
agentFile,
|
|
371
|
+
createdFiles,
|
|
372
|
+
extractionDisabled,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Load session history from .squad/sessions/ directory.
|
|
377
|
+
*
|
|
378
|
+
* @param squadDir - Path to project .squad/ directory
|
|
379
|
+
* @returns AgentHistory with entries from session files
|
|
380
|
+
*/
|
|
381
|
+
export function loadSessionHistory(squadDir) {
|
|
382
|
+
const sessionsDir = path.join(squadDir, 'sessions');
|
|
383
|
+
const entries = [];
|
|
384
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
385
|
+
return { entries };
|
|
386
|
+
}
|
|
387
|
+
const files = fs.readdirSync(sessionsDir)
|
|
388
|
+
.filter(f => f.endsWith('.json'))
|
|
389
|
+
.sort();
|
|
390
|
+
for (const file of files) {
|
|
391
|
+
try {
|
|
392
|
+
const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
|
|
393
|
+
const session = JSON.parse(content);
|
|
394
|
+
// Extract learnings from session data
|
|
395
|
+
if (session.learnings && Array.isArray(session.learnings)) {
|
|
396
|
+
for (const learning of session.learnings) {
|
|
397
|
+
entries.push({
|
|
398
|
+
id: learning.id || `${file}-${entries.length}`,
|
|
399
|
+
timestamp: learning.timestamp || session.timestamp || new Date().toISOString(),
|
|
400
|
+
type: learning.type || 'pattern',
|
|
401
|
+
content: learning.content || String(learning),
|
|
402
|
+
agent: learning.agent,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Extract decisions
|
|
407
|
+
if (session.decisions && Array.isArray(session.decisions)) {
|
|
408
|
+
for (const decision of session.decisions) {
|
|
409
|
+
entries.push({
|
|
410
|
+
id: decision.id || `${file}-decision-${entries.length}`,
|
|
411
|
+
timestamp: decision.timestamp || session.timestamp || new Date().toISOString(),
|
|
412
|
+
type: 'decision',
|
|
413
|
+
content: decision.content || String(decision),
|
|
414
|
+
agent: decision.agent,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Skip malformed session files
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { entries };
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Extract learnings from a consult mode session.
|
|
427
|
+
*
|
|
428
|
+
* Reads staged learnings from .squad/extract/ (classified by Scribe during session)
|
|
429
|
+
* and optionally merges approved items to your personal squad.
|
|
430
|
+
*
|
|
431
|
+
* @param options - Extraction options
|
|
432
|
+
* @returns Extraction result with learnings, merge stats, and paths
|
|
433
|
+
* @throws Error if not in consult mode or license blocks extraction
|
|
434
|
+
*/
|
|
435
|
+
export async function extractLearnings(options = {}) {
|
|
436
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
437
|
+
const personalSquadRoot = options.personalSquadRoot || getPersonalSquadRoot();
|
|
438
|
+
const dryRun = options.dryRun ?? false;
|
|
439
|
+
const clean = options.clean ?? false;
|
|
440
|
+
const acceptRisks = options.acceptRisks ?? false;
|
|
441
|
+
const force = options.force ?? false;
|
|
442
|
+
const squadDir = path.resolve(projectRoot, '.squad');
|
|
443
|
+
const projectName = options.projectName || path.basename(projectRoot);
|
|
444
|
+
// Check if we're in consult mode
|
|
445
|
+
if (!fs.existsSync(squadDir)) {
|
|
446
|
+
throw new Error('Not in consult mode. No .squad/ directory found.');
|
|
447
|
+
}
|
|
448
|
+
const configPath = path.join(squadDir, 'config.json');
|
|
449
|
+
if (!fs.existsSync(configPath)) {
|
|
450
|
+
throw new Error('Invalid consult mode: missing config.json');
|
|
451
|
+
}
|
|
452
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
453
|
+
if (!config.consult) {
|
|
454
|
+
throw new Error('This project has a .squad/ but is not in consult mode. Use normal squad commands.');
|
|
455
|
+
}
|
|
456
|
+
// Check if extraction is disabled for this consult session
|
|
457
|
+
if (config.extractionDisabled && !force) {
|
|
458
|
+
throw new ExtractionDisabledError();
|
|
459
|
+
}
|
|
460
|
+
// Detect license
|
|
461
|
+
const licensePath = path.join(projectRoot, 'LICENSE');
|
|
462
|
+
const licenseContent = fs.existsSync(licensePath)
|
|
463
|
+
? fs.readFileSync(licensePath, 'utf-8')
|
|
464
|
+
: '';
|
|
465
|
+
const license = detectLicense(licenseContent);
|
|
466
|
+
// Block copyleft extraction unless --accept-risks
|
|
467
|
+
const blocked = license.type === 'copyleft' && !acceptRisks;
|
|
468
|
+
// Get repository URL for logging
|
|
469
|
+
const repoUrl = getGitRemoteUrl(projectRoot);
|
|
470
|
+
if (blocked) {
|
|
471
|
+
return {
|
|
472
|
+
extracted: [],
|
|
473
|
+
skipped: [],
|
|
474
|
+
license,
|
|
475
|
+
projectName,
|
|
476
|
+
repoUrl,
|
|
477
|
+
timestamp: new Date().toISOString(),
|
|
478
|
+
acceptedRisks: false,
|
|
479
|
+
blocked: true,
|
|
480
|
+
decisionsMerged: 0,
|
|
481
|
+
skillsCreated: 0,
|
|
482
|
+
cleaned: false,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// Load staged learnings from .squad/extract/
|
|
486
|
+
let staged = loadStagedLearnings(squadDir);
|
|
487
|
+
// If interactive selection callback provided, let user choose
|
|
488
|
+
let skipped = [];
|
|
489
|
+
if (options.selectLearnings && staged.length > 0) {
|
|
490
|
+
const selected = await options.selectLearnings(staged);
|
|
491
|
+
const selectedFilenames = new Set(selected.map(l => l.filename));
|
|
492
|
+
skipped = staged.filter(l => !selectedFilenames.has(l.filename));
|
|
493
|
+
staged = selected;
|
|
494
|
+
}
|
|
495
|
+
const result = {
|
|
496
|
+
extracted: staged,
|
|
497
|
+
skipped,
|
|
498
|
+
license,
|
|
499
|
+
projectName,
|
|
500
|
+
repoUrl,
|
|
501
|
+
timestamp: new Date().toISOString(),
|
|
502
|
+
acceptedRisks: acceptRisks,
|
|
503
|
+
};
|
|
504
|
+
let decisionsMerged = 0;
|
|
505
|
+
let skillsCreated = 0;
|
|
506
|
+
let consultationLogPath;
|
|
507
|
+
let cleaned = false;
|
|
508
|
+
if (!dryRun && staged.length > 0) {
|
|
509
|
+
// Merge to personal squad
|
|
510
|
+
const mergeResult = await mergeToPersonalSquad(staged, personalSquadRoot);
|
|
511
|
+
decisionsMerged = mergeResult.decisions;
|
|
512
|
+
skillsCreated = mergeResult.skills;
|
|
513
|
+
// Log consultation
|
|
514
|
+
consultationLogPath = await logConsultation(personalSquadRoot, result);
|
|
515
|
+
// Remove extracted files from .squad/extract/
|
|
516
|
+
for (const learning of staged) {
|
|
517
|
+
fs.rmSync(learning.filepath, { force: true });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Clean up entire .squad/ if requested
|
|
521
|
+
if (clean && !dryRun) {
|
|
522
|
+
fs.rmSync(squadDir, { recursive: true, force: true });
|
|
523
|
+
cleaned = true;
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
...result,
|
|
527
|
+
blocked: false,
|
|
528
|
+
decisionsMerged,
|
|
529
|
+
skillsCreated,
|
|
530
|
+
consultationLogPath,
|
|
531
|
+
cleaned,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const COPYLEFT_LICENSES = [
|
|
535
|
+
'GPL',
|
|
536
|
+
'AGPL',
|
|
537
|
+
'LGPL',
|
|
538
|
+
'MPL',
|
|
539
|
+
'EPL',
|
|
540
|
+
'CDDL',
|
|
541
|
+
'CC-BY-SA',
|
|
542
|
+
];
|
|
543
|
+
const PERMISSIVE_LICENSES = [
|
|
544
|
+
'MIT',
|
|
545
|
+
'Apache',
|
|
546
|
+
'BSD',
|
|
547
|
+
'ISC',
|
|
548
|
+
'Unlicense',
|
|
549
|
+
'CC0',
|
|
550
|
+
'WTFPL',
|
|
551
|
+
];
|
|
552
|
+
/**
|
|
553
|
+
* Escape a string so it can be safely used inside a RegExp pattern.
|
|
554
|
+
*/
|
|
555
|
+
function escapeRegex(value) {
|
|
556
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Detect license type from LICENSE file content.
|
|
560
|
+
*
|
|
561
|
+
* @param licenseContent - Raw content of the LICENSE file
|
|
562
|
+
* @returns License classification with type, optional SPDX ID, and name
|
|
563
|
+
*/
|
|
564
|
+
export function detectLicense(licenseContent) {
|
|
565
|
+
const content = licenseContent;
|
|
566
|
+
const upperContent = licenseContent.toUpperCase();
|
|
567
|
+
// 1. Prefer SPDX identifiers when present.
|
|
568
|
+
const spdxMatch = content.match(/SPDX-License-Identifier:\s*([^\s*]+)/i);
|
|
569
|
+
if (spdxMatch && spdxMatch[1]) {
|
|
570
|
+
const spdxId = spdxMatch[1];
|
|
571
|
+
const spdxIdUpper = spdxId.toUpperCase();
|
|
572
|
+
const copyleftUpper = COPYLEFT_LICENSES.map(id => id.toUpperCase());
|
|
573
|
+
const permissiveUpper = PERMISSIVE_LICENSES.map(id => id.toUpperCase());
|
|
574
|
+
// Check for copyleft first (LGPL should match before GPL)
|
|
575
|
+
for (const license of copyleftUpper) {
|
|
576
|
+
if (spdxIdUpper.includes(license)) {
|
|
577
|
+
return { type: 'copyleft', spdxId, name: spdxId };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const license of permissiveUpper) {
|
|
581
|
+
if (spdxIdUpper.includes(license)) {
|
|
582
|
+
return { type: 'permissive', spdxId, name: spdxId };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return { type: 'unknown', spdxId, name: spdxId };
|
|
586
|
+
}
|
|
587
|
+
// 2. Fallback: word-boundary regex, longest-first to avoid
|
|
588
|
+
// misclassifying e.g. "LGPL" as "GPL".
|
|
589
|
+
const detectFromList = (licenses, type) => {
|
|
590
|
+
const sorted = [...licenses].sort((a, b) => b.length - a.length);
|
|
591
|
+
for (const license of sorted) {
|
|
592
|
+
const pattern = new RegExp(`\\b${escapeRegex(license.toUpperCase())}\\b`, 'i');
|
|
593
|
+
if (pattern.test(upperContent)) {
|
|
594
|
+
return { type, spdxId: license, name: license };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
};
|
|
599
|
+
// Check copyleft first (more restrictive)
|
|
600
|
+
const copyleftMatch = detectFromList(COPYLEFT_LICENSES, 'copyleft');
|
|
601
|
+
if (copyleftMatch)
|
|
602
|
+
return copyleftMatch;
|
|
603
|
+
const permissiveMatch = detectFromList(PERMISSIVE_LICENSES, 'permissive');
|
|
604
|
+
if (permissiveMatch)
|
|
605
|
+
return permissiveMatch;
|
|
606
|
+
return { type: 'unknown' };
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Load staged learnings from .squad/extract/ directory.
|
|
610
|
+
* These are generic learnings that Scribe classified during the session.
|
|
611
|
+
*
|
|
612
|
+
* @param squadDir - Path to project .squad/ directory
|
|
613
|
+
* @returns Array of staged learnings
|
|
614
|
+
*/
|
|
615
|
+
export function loadStagedLearnings(squadDir) {
|
|
616
|
+
const extractDir = path.join(squadDir, 'extract');
|
|
617
|
+
const learnings = [];
|
|
618
|
+
if (!fs.existsSync(extractDir)) {
|
|
619
|
+
return learnings;
|
|
620
|
+
}
|
|
621
|
+
const files = fs.readdirSync(extractDir).filter(f => f.endsWith('.md'));
|
|
622
|
+
for (const file of files) {
|
|
623
|
+
const filepath = path.join(extractDir, file);
|
|
624
|
+
try {
|
|
625
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
626
|
+
learnings.push({
|
|
627
|
+
filename: file,
|
|
628
|
+
filepath,
|
|
629
|
+
content,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// Skip unreadable files
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return learnings;
|
|
637
|
+
}
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Consultation Logging
|
|
640
|
+
// ============================================================================
|
|
641
|
+
/**
|
|
642
|
+
* Write or append a consultation log entry to the personal squad.
|
|
643
|
+
*
|
|
644
|
+
* Creates the consultations directory if it doesn't exist.
|
|
645
|
+
* For new projects, creates a full header; for existing projects, appends session entry.
|
|
646
|
+
*
|
|
647
|
+
* @param personalSquadRoot - Path to personal squad root (e.g. ~/.config/squad/.squad)
|
|
648
|
+
* @param result - Extraction result with learnings and metadata
|
|
649
|
+
* @returns Path to the consultation log file
|
|
650
|
+
*/
|
|
651
|
+
export async function logConsultation(personalSquadRoot, result) {
|
|
652
|
+
const consultDir = path.join(personalSquadRoot, 'consultations');
|
|
653
|
+
const logPath = path.join(consultDir, `${result.projectName}.md`);
|
|
654
|
+
// Create consultations directory if needed
|
|
655
|
+
if (!fs.existsSync(consultDir)) {
|
|
656
|
+
fs.mkdirSync(consultDir, { recursive: true });
|
|
657
|
+
}
|
|
658
|
+
const today = result.timestamp.split('T')[0] ?? new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
659
|
+
if (fs.existsSync(logPath)) {
|
|
660
|
+
// Append to existing log — update "Last session" and add new entry
|
|
661
|
+
let content = fs.readFileSync(logPath, 'utf-8');
|
|
662
|
+
// Update "Last session" date
|
|
663
|
+
content = content.replace(/\*\*Last session:\*\* \d{4}-\d{2}-\d{2}/, `**Last session:** ${today}`);
|
|
664
|
+
// Build session entry
|
|
665
|
+
const sessionEntry = formatSessionEntry(result, today);
|
|
666
|
+
// Append to file
|
|
667
|
+
fs.writeFileSync(logPath, content + sessionEntry, 'utf-8');
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// Create new consultation log with full header
|
|
671
|
+
const header = formatLogHeader(result, today);
|
|
672
|
+
const sessionEntry = formatSessionEntry(result, today);
|
|
673
|
+
fs.writeFileSync(logPath, header + sessionEntry, 'utf-8');
|
|
674
|
+
}
|
|
675
|
+
return logPath;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Format the header for a new consultation log file.
|
|
679
|
+
*/
|
|
680
|
+
function formatLogHeader(result, date) {
|
|
681
|
+
const repoLine = result.repoUrl
|
|
682
|
+
? `**Repository:** ${result.repoUrl}\n`
|
|
683
|
+
: '';
|
|
684
|
+
const licenseName = result.license.spdxId || result.license.name || result.license.type;
|
|
685
|
+
return `# ${result.projectName}
|
|
686
|
+
|
|
687
|
+
${repoLine}**First consulted:** ${date}
|
|
688
|
+
**Last session:** ${date}
|
|
689
|
+
**License:** ${licenseName}
|
|
690
|
+
|
|
691
|
+
## Extracted Learnings
|
|
692
|
+
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Format a session entry for the consultation log.
|
|
697
|
+
*/
|
|
698
|
+
function formatSessionEntry(result, date) {
|
|
699
|
+
if (result.extracted.length === 0) {
|
|
700
|
+
return `### ${date}
|
|
701
|
+
- No learnings extracted
|
|
702
|
+
|
|
703
|
+
`;
|
|
704
|
+
}
|
|
705
|
+
// Just list titles/filenames, not content
|
|
706
|
+
const lines = result.extracted.map(l => `- ${l.filename}`);
|
|
707
|
+
return `### ${date}
|
|
708
|
+
${lines.join('\n')}
|
|
709
|
+
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
// ============================================================================
|
|
713
|
+
// Merge to Personal Squad
|
|
714
|
+
// ============================================================================
|
|
715
|
+
/**
|
|
716
|
+
* Check if content looks like a skill (has YAML frontmatter with skill markers).
|
|
717
|
+
*/
|
|
718
|
+
function isSkillContent(content) {
|
|
719
|
+
// Skills have YAML frontmatter with name/confidence/domain
|
|
720
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
721
|
+
if (!frontmatterMatch || !frontmatterMatch[1])
|
|
722
|
+
return false;
|
|
723
|
+
const frontmatter = frontmatterMatch[1];
|
|
724
|
+
// Must have at least name and confidence to be a skill
|
|
725
|
+
return frontmatter.includes('name:') && frontmatter.includes('confidence:');
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Extract skill name from YAML frontmatter.
|
|
729
|
+
*/
|
|
730
|
+
function extractSkillName(content) {
|
|
731
|
+
const match = content.match(/^---\n[\s\S]*?name:\s*["']?([^"'\n]+)["']?/);
|
|
732
|
+
return match && match[1] ? match[1].trim() : null;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Merge staged learnings into personal squad.
|
|
736
|
+
*
|
|
737
|
+
* Routes skills to ~/.squad/skills/{name}/SKILL.md
|
|
738
|
+
* Routes decisions to ~/.squad/decisions.md (with smart merge)
|
|
739
|
+
*
|
|
740
|
+
* @param learnings - Staged learnings to merge
|
|
741
|
+
* @param personalSquadRoot - Path to personal squad root
|
|
742
|
+
*/
|
|
743
|
+
export async function mergeToPersonalSquad(learnings, personalSquadRoot) {
|
|
744
|
+
if (learnings.length === 0) {
|
|
745
|
+
return { decisions: 0, skills: 0 };
|
|
746
|
+
}
|
|
747
|
+
let decisionsAdded = 0;
|
|
748
|
+
let skillsAdded = 0;
|
|
749
|
+
const decisions = [];
|
|
750
|
+
const skills = [];
|
|
751
|
+
// Classify learnings
|
|
752
|
+
for (const learning of learnings) {
|
|
753
|
+
if (isSkillContent(learning.content)) {
|
|
754
|
+
skills.push(learning);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
decisions.push(learning);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Route skills to ~/.squad/skills/{name}/SKILL.md
|
|
761
|
+
const skillsDir = path.join(personalSquadRoot, 'skills');
|
|
762
|
+
for (const skill of skills) {
|
|
763
|
+
const skillName = extractSkillName(skill.content) || skill.filename.replace('.md', '');
|
|
764
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
765
|
+
// Create skill directory if needed
|
|
766
|
+
if (!fs.existsSync(skillDir)) {
|
|
767
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
768
|
+
}
|
|
769
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
770
|
+
// Write skill (overwrites if exists — newer extraction wins)
|
|
771
|
+
fs.writeFileSync(skillPath, skill.content, 'utf-8');
|
|
772
|
+
skillsAdded++;
|
|
773
|
+
}
|
|
774
|
+
// Route decisions to ~/.squad/decisions.md
|
|
775
|
+
if (decisions.length > 0) {
|
|
776
|
+
const decisionsPath = path.join(personalSquadRoot, 'decisions.md');
|
|
777
|
+
const newContent = decisions.map(d => d.content.trim()).join('\n\n');
|
|
778
|
+
if (fs.existsSync(decisionsPath)) {
|
|
779
|
+
const existing = fs.readFileSync(decisionsPath, 'utf-8');
|
|
780
|
+
// Check if we already have an "Extracted from Consultations" section
|
|
781
|
+
if (existing.includes('## Extracted from Consultations')) {
|
|
782
|
+
// Append under the existing section (before any subsequent ## heading)
|
|
783
|
+
const parts = existing.split('## Extracted from Consultations');
|
|
784
|
+
const beforeSection = parts[0];
|
|
785
|
+
const afterSection = parts[1] ?? '';
|
|
786
|
+
// Find where the next section starts (if any)
|
|
787
|
+
const nextSectionMatch = afterSection.match(/\n## /);
|
|
788
|
+
if (nextSectionMatch && nextSectionMatch.index !== undefined) {
|
|
789
|
+
// Insert before next section
|
|
790
|
+
const sectionContent = afterSection.slice(0, nextSectionMatch.index);
|
|
791
|
+
const rest = afterSection.slice(nextSectionMatch.index);
|
|
792
|
+
fs.writeFileSync(decisionsPath, beforeSection +
|
|
793
|
+
'## Extracted from Consultations' +
|
|
794
|
+
sectionContent.trimEnd() +
|
|
795
|
+
'\n\n' +
|
|
796
|
+
newContent +
|
|
797
|
+
'\n' +
|
|
798
|
+
rest, 'utf-8');
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
// No next section — append to end
|
|
802
|
+
fs.writeFileSync(decisionsPath, existing.trimEnd() + '\n\n' + newContent + '\n', 'utf-8');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// No extraction section yet — create one
|
|
807
|
+
fs.writeFileSync(decisionsPath, existing.trimEnd() + '\n\n## Extracted from Consultations\n\n' + newContent + '\n', 'utf-8');
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
// Create new decisions file
|
|
812
|
+
fs.writeFileSync(decisionsPath, `# Squad Decisions\n\n## Extracted from Consultations\n\n${newContent}\n`, 'utf-8');
|
|
813
|
+
}
|
|
814
|
+
decisionsAdded = decisions.length;
|
|
815
|
+
}
|
|
816
|
+
return { decisions: decisionsAdded, skills: skillsAdded };
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=consult.js.map
|