@refactco/refact-os 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/bin/refact-os.js +154 -0
  5. package/lib/adapters.js +302 -0
  6. package/lib/company.js +76 -0
  7. package/lib/frontmatter.js +30 -0
  8. package/lib/migrate.js +116 -0
  9. package/lib/project-utils.js +179 -0
  10. package/lib/refact-config.js +324 -0
  11. package/lib/scaffold.js +329 -0
  12. package/lib/validate.js +145 -0
  13. package/package.json +46 -0
  14. package/templates/base/AGENTS.md +9 -0
  15. package/templates/base/CLAUDE.md +3 -0
  16. package/templates/base/README.md +54 -0
  17. package/templates/base/agent/AGENTS.md +60 -0
  18. package/templates/base/agent/CLAUDE.md +7 -0
  19. package/templates/base/agent/claude-hooks.json +32 -0
  20. package/templates/base/agent/hooks/claude-sync-transcript.py +236 -0
  21. package/templates/base/agent/hooks/preflight-metadata.mjs +202 -0
  22. package/templates/base/agent/hooks/send-transcript-to-remote-server.py +238 -0
  23. package/templates/base/agent/hooks/sync-chat-transcript.py +188 -0
  24. package/templates/base/agent/hooks.json +29 -0
  25. package/templates/base/agent/scripts/import-project-chat-history.py +196 -0
  26. package/templates/base/agent/scripts/sync-asana.mjs +408 -0
  27. package/templates/base/agent/skills/adopt/SKILL.md +46 -0
  28. package/templates/base/agent/skills/close-ticket/SKILL.md +31 -0
  29. package/templates/base/agent/skills/extract-learnings/SKILL.md +90 -0
  30. package/templates/base/agent/skills/git-it/SKILL.md +138 -0
  31. package/templates/base/agent/skills/import-chat-history/SKILL.md +85 -0
  32. package/templates/base/agent/skills/ingest-input/SKILL.md +43 -0
  33. package/templates/base/agent/skills/open-ticket/SKILL.md +36 -0
  34. package/templates/base/agent/skills/process-docs/SKILL.md +69 -0
  35. package/templates/base/agent/skills/project-status/SKILL.md +35 -0
  36. package/templates/base/agent/skills/project-status/scripts/scan-status.mjs +153 -0
  37. package/templates/base/agent/skills/refact/SKILL.md +139 -0
  38. package/templates/base/agent/skills/setup-project/SKILL.md +140 -0
  39. package/templates/base/agent/skills/sync-asana/SKILL.md +106 -0
  40. package/templates/base/agent/skills/update-canonical-record/SKILL.md +28 -0
  41. package/templates/base/agent/skills/update-package/SKILL.md +51 -0
  42. package/templates/base/docs/context/project.md +30 -0
  43. package/templates/base/docs/decisions.md +22 -0
  44. package/templates/base/docs/index.md +31 -0
  45. package/templates/base/docs/sources/raw/.gitkeep +0 -0
  46. package/templates/base/docs/task/.gitkeep +0 -0
  47. package/templates/base/env.example +14 -0
  48. package/templates/base/gitignore +34 -0
  49. package/templates/overlays/client/agent/skills/create-deliverable/SKILL.md +29 -0
  50. package/templates/overlays/client/docs/deliverables/.gitkeep +0 -0
  51. package/templates/overlays/code/agent/skills/add-codebase/SKILL.md +239 -0
  52. package/templates/overlays/code/agent/skills/code-development/SKILL.md +58 -0
  53. package/templates/overlays/code/agent/skills/code-development/references/gitflow.md +144 -0
  54. package/templates/overlays/nextjs/agent/skills/nextjs-dev/SKILL.md +93 -0
  55. package/templates/overlays/nextjs/agent/skills/setup-netlify-deploy/SKILL.md +143 -0
  56. package/templates/overlays/nextjs/agent/skills/setup-nextjs-app/SKILL.md +118 -0
  57. package/templates/overlays/nextjs/agent/skills/setup-vercel-deploy/SKILL.md +116 -0
  58. package/templates/overlays/wordpress/agent/skills/install-wp-skills/SKILL.md +130 -0
  59. package/templates/overlays/wordpress/agent/skills/setup-kinsta-deploy/SKILL.md +201 -0
  60. package/templates/overlays/wordpress/agent/skills/wp-env/SKILL.md +478 -0
  61. package/templates/overlays/wordpress/wp-cli.yml.example +46 -0
@@ -0,0 +1,179 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const readline = require("readline/promises");
4
+
5
+ const VALID_PROJECT_TYPES = ["wordpress", "nextjs", "blank"];
6
+ // Legacy names accepted on input and folded onto the current ones, so configs
7
+ // and commands from before the rename keep working. "other" was renamed "blank".
8
+ const PROJECT_TYPE_ALIASES = { other: "blank" };
9
+
10
+ function normalizeProjectTypes(raw) {
11
+ const values = (Array.isArray(raw) ? raw : [raw]).flatMap((value) => String(value || "").split(","));
12
+ const seen = new Set();
13
+ for (const value of values) {
14
+ let type = String(value || "").trim().toLowerCase();
15
+ if (PROJECT_TYPE_ALIASES[type]) type = PROJECT_TYPE_ALIASES[type];
16
+ if (!VALID_PROJECT_TYPES.includes(type)) continue;
17
+ seen.add(type);
18
+ }
19
+ if (seen.size === 0) return [];
20
+ // "blank" is the no-specific-stack fallback; any real type supersedes it.
21
+ if (seen.size > 1 && seen.has("blank")) seen.delete("blank");
22
+ return [...seen];
23
+ }
24
+
25
+ function primaryProjectType(projectTypes) {
26
+ const normalized = normalizeProjectTypes(projectTypes);
27
+ return normalized[0] || "blank";
28
+ }
29
+
30
+ function detectProjectTypes(targetDir) {
31
+ const types = [];
32
+ const has = (p) => fs.existsSync(path.join(targetDir, p));
33
+ if (has("apps/wordpress/wp-content") || has("wp-content") || has(".wp-env.json")) {
34
+ types.push("wordpress");
35
+ }
36
+ if (has("next.config.js") || has("next.config.mjs") || has("next.config.ts")) {
37
+ types.push("nextjs");
38
+ }
39
+ try {
40
+ const packageJsonPath = path.join(targetDir, "package.json");
41
+ if (fs.existsSync(packageJsonPath)) {
42
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
43
+ if ((pkg.dependencies && pkg.dependencies.next) || (pkg.devDependencies && pkg.devDependencies.next)) {
44
+ types.push("nextjs");
45
+ }
46
+ }
47
+ } catch (_err) {
48
+ // Best effort; fall back to "blank".
49
+ }
50
+ return normalizeProjectTypes(types.length > 0 ? types : ["blank"]);
51
+ }
52
+
53
+ function detectProjectType(targetDir) {
54
+ return primaryProjectType(detectProjectTypes(targetDir));
55
+ }
56
+
57
+ async function askProjectTypes(defaultTypes) {
58
+ const normalizedDefault = normalizeProjectTypes(defaultTypes);
59
+ const defaultLabel = normalizedDefault.join(",") || "blank";
60
+ const rl = readline.createInterface({
61
+ input: process.stdin,
62
+ output: process.stdout,
63
+ });
64
+ const prompt = `Project type(s) [wordpress/nextjs/blank, comma-separated] (default: ${defaultLabel}): `;
65
+ const answer = (await rl.question(prompt)).trim().toLowerCase();
66
+ rl.close();
67
+ const parsed = normalizeProjectTypes(answer);
68
+ return parsed.length > 0 ? parsed : normalizedDefault;
69
+ }
70
+
71
+ async function askProjectType(defaultType) {
72
+ return primaryProjectType(await askProjectTypes([defaultType]));
73
+ }
74
+
75
+ function writeFile(targetPath, content, options = {}) {
76
+ const { force = true } = options;
77
+ if (!force && fs.existsSync(targetPath)) {
78
+ return false;
79
+ }
80
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
81
+ fs.writeFileSync(targetPath, content, "utf8");
82
+ return true;
83
+ }
84
+
85
+ function _safePackageNameFromDir(targetDir) {
86
+ return (
87
+ path
88
+ .basename(targetDir)
89
+ .toLowerCase()
90
+ .replace(/[^a-z0-9-_.]+/g, "-")
91
+ .replace(/^-+/, "")
92
+ .replace(/-+$/, "") || "refact-project"
93
+ );
94
+ }
95
+
96
+ const SCAFFOLD_SCRIPTS = {
97
+ "chats:import": "python3 .cursor/scripts/import-project-chat-history.py",
98
+ "chats:import:dry": "python3 .cursor/scripts/import-project-chat-history.py --dry-run",
99
+ "asana:sync": "node .cursor/scripts/sync-asana.mjs",
100
+ "asana:sync:dry": "node .cursor/scripts/sync-asana.mjs --dry-run",
101
+ "refact:sync": "refact-os sync",
102
+ "refact:validate": "refact-os validate",
103
+ "refact:migrate": "refact-os migrate",
104
+ "refact:update": "refact-os init",
105
+ };
106
+
107
+ // refact-os is distributed from GitHub (not the npm registry). Adding it as a
108
+ // devDependency is what lets the `refact:*` npm scripts above call the bare
109
+ // `refact-os` binary (instead of fetching it via npx on every run).
110
+ const SCAFFOLD_DEV_DEPENDENCIES = {
111
+ "refact-os": "github:refactco/refact-os",
112
+ };
113
+
114
+ // Add scaffold devDependencies that aren't already declared (in either
115
+ // dependencies or devDependencies). Non-destructive; mutates `data` and
116
+ // returns the names added.
117
+ function _ensureDevDependencies(data) {
118
+ const depsAdded = [];
119
+ const deps = data.dependencies || {};
120
+ const devDeps = data.devDependencies || {};
121
+ for (const [name, spec] of Object.entries(SCAFFOLD_DEV_DEPENDENCIES)) {
122
+ if (name in deps || name in devDeps) continue;
123
+ devDeps[name] = spec;
124
+ depsAdded.push(name);
125
+ }
126
+ if (depsAdded.length > 0) data.devDependencies = devDeps;
127
+ return depsAdded;
128
+ }
129
+
130
+ function ensurePackageScripts(targetDir) {
131
+ const packageJsonPath = path.join(targetDir, "package.json");
132
+ if (!fs.existsSync(packageJsonPath)) {
133
+ const scaffoldPkg = {
134
+ name: _safePackageNameFromDir(targetDir),
135
+ version: "1.0.0",
136
+ private: true,
137
+ scripts: { ...SCAFFOLD_SCRIPTS },
138
+ devDependencies: { ...SCAFFOLD_DEV_DEPENDENCIES },
139
+ };
140
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(scaffoldPkg, null, 2)}\n`, "utf8");
141
+ return {
142
+ status: "created",
143
+ scriptsAdded: Object.keys(SCAFFOLD_SCRIPTS),
144
+ depsAdded: Object.keys(SCAFFOLD_DEV_DEPENDENCIES),
145
+ };
146
+ }
147
+ try {
148
+ const data = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
149
+ const scripts = data.scripts || {};
150
+ const scriptsAdded = [];
151
+ for (const [name, command] of Object.entries(SCAFFOLD_SCRIPTS)) {
152
+ if (!scripts[name]) {
153
+ scripts[name] = command;
154
+ scriptsAdded.push(name);
155
+ }
156
+ }
157
+ const depsAdded = _ensureDevDependencies(data);
158
+ if (scriptsAdded.length > 0 || depsAdded.length > 0) {
159
+ if (scriptsAdded.length > 0) data.scripts = scripts;
160
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
161
+ return { status: "updated", scriptsAdded, depsAdded };
162
+ }
163
+ return { status: "unchanged", scriptsAdded: [], depsAdded: [] };
164
+ } catch (_err) {
165
+ return { status: "invalid", scriptsAdded: [], depsAdded: [] };
166
+ }
167
+ }
168
+
169
+ module.exports = {
170
+ VALID_PROJECT_TYPES,
171
+ normalizeProjectTypes,
172
+ primaryProjectType,
173
+ detectProjectTypes,
174
+ detectProjectType,
175
+ askProjectTypes,
176
+ askProjectType,
177
+ writeFile,
178
+ ensurePackageScripts,
179
+ };
@@ -0,0 +1,324 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const readline = require("readline/promises");
4
+ const { normalizeProjectTypes, primaryProjectType } = require("./project-utils");
5
+
6
+ const CONFIG_FILENAME = ".refact-os.json";
7
+
8
+ // The `.refact-os.json` schema. `stack` is the single source of truth for which
9
+ // project types this repo is: its KEYS are the type list (no separate
10
+ // projectType/projectTypes). Each type entry carries its own hosting, runtime,
11
+ // and per-environment deploy routing — see `stackTypeTemplate()`.
12
+ const REQUIRED_FIELDS = [
13
+ {
14
+ path: "stack",
15
+ label: "Project stack",
16
+ type: "stack",
17
+ required: true,
18
+ prompt: "Project type(s) [wordpress/nextjs/blank, comma-separated]",
19
+ },
20
+ {
21
+ path: "asana.projectId",
22
+ label: "Asana project ID",
23
+ type: "string",
24
+ required: false,
25
+ prompt:
26
+ "Asana project ID (numeric, from app.asana.com/0/<id>/...) — leave blank or type 'skip' to mark as not used",
27
+ },
28
+ ];
29
+
30
+ // Shape of a single `stack.<type>` entry at scaffold time. hosting/runtime are
31
+ // filled by `/refact init`; `environments` is keyed by env name (production,
32
+ // staging, …) with { url, branch, ssh? } each — ssh is present only for
33
+ // SSH-push hosts (Kinsta, WP Engine) and absent for git-integration hosts
34
+ // (Vercel, Netlify). Secrets never live here; only non-secret routing.
35
+ function stackTypeTemplate() {
36
+ return { hosting: null, runtime: null, environments: {} };
37
+ }
38
+
39
+ function _get(obj, dotted) {
40
+ return dotted.split(".").reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
41
+ }
42
+
43
+ function _set(obj, dotted, value) {
44
+ const keys = dotted.split(".");
45
+ let cur = obj;
46
+ for (let i = 0; i < keys.length - 1; i += 1) {
47
+ const k = keys[i];
48
+ if (cur[k] == null || typeof cur[k] !== "object") {
49
+ cur[k] = {};
50
+ }
51
+ cur = cur[k];
52
+ }
53
+ cur[keys[keys.length - 1]] = value;
54
+ }
55
+
56
+ function _has(obj, dotted) {
57
+ const keys = dotted.split(".");
58
+ let cur = obj;
59
+ for (let i = 0; i < keys.length; i += 1) {
60
+ if (cur == null || typeof cur !== "object" || !(keys[i] in cur)) {
61
+ return false;
62
+ }
63
+ cur = cur[keys[i]];
64
+ }
65
+ return true;
66
+ }
67
+
68
+ function configPath(targetDir) {
69
+ return path.join(targetDir, CONFIG_FILENAME);
70
+ }
71
+
72
+ function loadConfig(targetDir) {
73
+ const p = configPath(targetDir);
74
+ if (!fs.existsSync(p)) {
75
+ return {};
76
+ }
77
+ try {
78
+ const raw = fs.readFileSync(p, "utf8");
79
+ const parsed = JSON.parse(raw);
80
+ return parsed && typeof parsed === "object" ? parsed : {};
81
+ } catch (_err) {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ function saveConfig(targetDir, config) {
87
+ const p = configPath(targetDir);
88
+ fs.writeFileSync(p, `${JSON.stringify(config, null, 2)}\n`, "utf8");
89
+ return p;
90
+ }
91
+
92
+ // The project's type list IS the set of keys under `stack`. Returns a
93
+ // normalized, deduped array (e.g. ["wordpress", "nextjs"]).
94
+ function getProjectTypes(config) {
95
+ if (!config || !config.stack || typeof config.stack !== "object" || Array.isArray(config.stack)) {
96
+ return [];
97
+ }
98
+ return normalizeProjectTypes(Object.keys(config.stack));
99
+ }
100
+
101
+ function findMissingFields(config) {
102
+ const missing = [];
103
+ for (const field of REQUIRED_FIELDS) {
104
+ if (field.required === false) continue; // optional fields never block
105
+ if (field.type === "stack") {
106
+ // Required means: stack exists and names at least one valid type.
107
+ if (getProjectTypes(config).length === 0) missing.push(field);
108
+ continue;
109
+ }
110
+ if (_has(config, field.path)) continue;
111
+ missing.push(field);
112
+ }
113
+ return missing;
114
+ }
115
+
116
+ function _parseProjectTypeFromAgents(agentsContent) {
117
+ const match = agentsContent.match(/-\s*Project type(?:\(s\))?:\s*`([^`]+)`/m);
118
+ if (!match) return null;
119
+ const val = match[1].trim();
120
+ if (!val || /^<.*>$/.test(val)) return null; // ignore unfilled `<placeholder>`
121
+ return val;
122
+ }
123
+
124
+ // Parse a `- <Label>: \`value\`` line out of an AGENTS.md "Stack at a glance"
125
+ // block, skipping unfilled `<placeholder>` values. Used only for migration.
126
+ function _parseAgentsField(agentsContent, label) {
127
+ const esc = label.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
128
+ const match = agentsContent.match(new RegExp(`-\\s*${esc}:\\s*\`([^\`]+)\``, "m"));
129
+ if (!match) return null;
130
+ const val = match[1].trim();
131
+ if (!val || /^<.*>$/.test(val)) return null;
132
+ return val;
133
+ }
134
+
135
+ function _readIfExists(p) {
136
+ try {
137
+ return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : null;
138
+ } catch (_err) {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function _stripScaffoldMetadataSection(agentsContent) {
144
+ const lines = agentsContent.split("\n");
145
+ const startIdx = lines.findIndex((line) => /^##\s+Scaffold Metadata\s*$/.test(line));
146
+ if (startIdx === -1) return agentsContent;
147
+ let endIdx = lines.length;
148
+ for (let i = startIdx + 1; i < lines.length; i += 1) {
149
+ if (/^##\s+/.test(lines[i])) {
150
+ endIdx = i;
151
+ break;
152
+ }
153
+ }
154
+ const before = lines.slice(0, startIdx).join("\n").replace(/\n+$/, "");
155
+ const after = lines.slice(endIdx).join("\n");
156
+ const joined = after ? `${before}\n\n${after}` : `${before}\n`;
157
+ return joined.replace(/\n{3,}/g, "\n\n");
158
+ }
159
+
160
+ // Ensure `config.stack` has a well-formed entry for the requested types. Existing
161
+ // entry detail (hosting/runtime/environments) is preserved; only missing sub-keys
162
+ // are backfilled. Invalid keys are dropped, a legacy "other" entry is migrated to
163
+ // "blank", and a stale "blank" is removed once a real type is present.
164
+ //
165
+ // With `options.replace` true, `defaultTypes` is AUTHORITATIVE: any type not in it
166
+ // is dropped (so an explicit/interactive choice can downgrade away from a
167
+ // previously-detected stack). Without it (detection / non-interactive re-run),
168
+ // the requested types are merged with whatever is already present.
169
+ function ensureStack(config, defaultTypes, options = {}) {
170
+ const replace = options.replace === true;
171
+ if (!config.stack || typeof config.stack !== "object" || Array.isArray(config.stack)) {
172
+ config.stack = {};
173
+ }
174
+ // Migrate a legacy "other" entry to its new name "blank", preserving detail.
175
+ if (config.stack.other) {
176
+ if (!config.stack.blank) config.stack.blank = config.stack.other;
177
+ delete config.stack.other;
178
+ }
179
+ // Drop keys that aren't valid project types.
180
+ for (const key of Object.keys(config.stack)) {
181
+ if (normalizeProjectTypes([key]).length === 0) delete config.stack[key];
182
+ }
183
+ const wanted = normalizeProjectTypes(defaultTypes);
184
+ let types;
185
+ if (replace) {
186
+ types = wanted.length > 0 ? wanted : ["blank"];
187
+ for (const key of Object.keys(config.stack)) {
188
+ if (!types.includes(key)) delete config.stack[key];
189
+ }
190
+ } else {
191
+ const union = normalizeProjectTypes([...Object.keys(config.stack), ...wanted]);
192
+ types = union.length > 0 ? union : ["blank"];
193
+ }
194
+ for (const type of types) {
195
+ const existing = config.stack[type];
196
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
197
+ config.stack[type] = stackTypeTemplate();
198
+ } else {
199
+ if (!("hosting" in existing)) existing.hosting = null;
200
+ if (!("runtime" in existing)) existing.runtime = null;
201
+ if (!existing.environments || typeof existing.environments !== "object" || Array.isArray(existing.environments)) {
202
+ existing.environments = {};
203
+ }
204
+ }
205
+ }
206
+ // normalizeProjectTypes drops "blank" when a real type is present; mirror that
207
+ // in the stored stack so it doesn't linger from an earlier scaffold.
208
+ if (Object.keys(config.stack).length > 1 && config.stack.blank) {
209
+ delete config.stack.blank;
210
+ }
211
+ return config;
212
+ }
213
+
214
+ // Bring an older-schema config up to the `stack` model. Folds legacy top-level
215
+ // `projectType` / `projectTypes` keys into `stack` and deletes them; if the
216
+ // config still has no stack, seeds it from the AGENTS.md "Project type" prose;
217
+ // best-effort lifts Hosting / runtime prose onto the primary type. Also strips
218
+ // the legacy "## Scaffold Metadata" section from AGENTS.md.
219
+ function migrateFromAgents(targetDir, config) {
220
+ let migrated = false;
221
+
222
+ const legacyTypeKeys = normalizeProjectTypes([
223
+ ...(Array.isArray(config.projectTypes) ? config.projectTypes : config.projectTypes ? [config.projectTypes] : []),
224
+ ...(config.projectType ? [config.projectType] : []),
225
+ ]);
226
+ if (getProjectTypes(config).length === 0 && legacyTypeKeys.length > 0) {
227
+ ensureStack(config, legacyTypeKeys);
228
+ migrated = true;
229
+ }
230
+ if ("projectType" in config) {
231
+ delete config.projectType;
232
+ migrated = true;
233
+ }
234
+ if ("projectTypes" in config) {
235
+ delete config.projectTypes;
236
+ migrated = true;
237
+ }
238
+
239
+ // The "Stack at a glance" prose lives in the contract (agent/AGENTS.md) in the
240
+ // current layout; older layouts kept it in the root AGENTS.md. Read the
241
+ // contract first and fall back to root for parsing.
242
+ const contractText = _readIfExists(path.join(targetDir, "agent", "AGENTS.md"));
243
+ const rootPath = path.join(targetDir, "AGENTS.md");
244
+ const rootText = _readIfExists(rootPath);
245
+ const proseSource = contractText || rootText;
246
+
247
+ if (proseSource) {
248
+ if (getProjectTypes(config).length === 0) {
249
+ const legacy = _parseProjectTypeFromAgents(proseSource);
250
+ if (legacy) {
251
+ ensureStack(config, normalizeProjectTypes(legacy));
252
+ migrated = true;
253
+ }
254
+ }
255
+ const entry = config.stack && config.stack[primaryProjectType(getProjectTypes(config))];
256
+ if (entry) {
257
+ if (entry.hosting == null) {
258
+ const hosting = _parseAgentsField(proseSource, "Hosting");
259
+ if (hosting) {
260
+ entry.hosting = hosting;
261
+ migrated = true;
262
+ }
263
+ }
264
+ if (entry.runtime == null) {
265
+ const runtime = _parseAgentsField(proseSource, "Local dev/runtime");
266
+ if (runtime) {
267
+ entry.runtime = runtime;
268
+ migrated = true;
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // Strip the legacy "## Scaffold Metadata" section from the root AGENTS.md.
275
+ if (rootText != null) {
276
+ const stripped = _stripScaffoldMetadataSection(rootText);
277
+ if (stripped !== rootText) {
278
+ fs.writeFileSync(rootPath, stripped, "utf8");
279
+ return { migrated, stripped: true };
280
+ }
281
+ }
282
+ return { migrated, stripped: false };
283
+ }
284
+
285
+ function setAsanaProjectId(config, raw) {
286
+ const trimmed = (raw || "").trim();
287
+ if (!trimmed || /^(skip|none|n\/a)$/i.test(trimmed)) {
288
+ _set(config, "asana.projectId", null);
289
+ _set(config, "asana.projectUrl", null);
290
+ return config;
291
+ }
292
+ _set(config, "asana.projectId", trimmed);
293
+ if (/^\d+$/.test(trimmed)) {
294
+ _set(config, "asana.projectUrl", `https://app.asana.com/0/${trimmed}`);
295
+ } else {
296
+ _set(config, "asana.projectUrl", null);
297
+ }
298
+ return config;
299
+ }
300
+
301
+ async function askAsanaProjectId() {
302
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
303
+ const answer = await rl.question(
304
+ "Asana project ID (numeric, from app.asana.com/0/<id>/...) — leave blank to skip: ",
305
+ );
306
+ rl.close();
307
+ return answer;
308
+ }
309
+
310
+ module.exports = {
311
+ CONFIG_FILENAME,
312
+ REQUIRED_FIELDS,
313
+ stackTypeTemplate,
314
+ configPath,
315
+ loadConfig,
316
+ saveConfig,
317
+ getProjectTypes,
318
+ findMissingFields,
319
+ migrateFromAgents,
320
+ ensureStack,
321
+ setAsanaProjectId,
322
+ askAsanaProjectId,
323
+ _internal: { _get, _set, _has, _parseProjectTypeFromAgents, _parseAgentsField, _stripScaffoldMetadataSection },
324
+ };