@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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/bin/refact-os.js +154 -0
- package/lib/adapters.js +302 -0
- package/lib/company.js +76 -0
- package/lib/frontmatter.js +30 -0
- package/lib/migrate.js +116 -0
- package/lib/project-utils.js +179 -0
- package/lib/refact-config.js +324 -0
- package/lib/scaffold.js +329 -0
- package/lib/validate.js +145 -0
- package/package.json +46 -0
- package/templates/base/AGENTS.md +9 -0
- package/templates/base/CLAUDE.md +3 -0
- package/templates/base/README.md +54 -0
- package/templates/base/agent/AGENTS.md +60 -0
- package/templates/base/agent/CLAUDE.md +7 -0
- package/templates/base/agent/claude-hooks.json +32 -0
- package/templates/base/agent/hooks/claude-sync-transcript.py +236 -0
- package/templates/base/agent/hooks/preflight-metadata.mjs +202 -0
- package/templates/base/agent/hooks/send-transcript-to-remote-server.py +238 -0
- package/templates/base/agent/hooks/sync-chat-transcript.py +188 -0
- package/templates/base/agent/hooks.json +29 -0
- package/templates/base/agent/scripts/import-project-chat-history.py +196 -0
- package/templates/base/agent/scripts/sync-asana.mjs +408 -0
- package/templates/base/agent/skills/adopt/SKILL.md +46 -0
- package/templates/base/agent/skills/close-ticket/SKILL.md +31 -0
- package/templates/base/agent/skills/extract-learnings/SKILL.md +90 -0
- package/templates/base/agent/skills/git-it/SKILL.md +138 -0
- package/templates/base/agent/skills/import-chat-history/SKILL.md +85 -0
- package/templates/base/agent/skills/ingest-input/SKILL.md +43 -0
- package/templates/base/agent/skills/open-ticket/SKILL.md +36 -0
- package/templates/base/agent/skills/process-docs/SKILL.md +69 -0
- package/templates/base/agent/skills/project-status/SKILL.md +35 -0
- package/templates/base/agent/skills/project-status/scripts/scan-status.mjs +153 -0
- package/templates/base/agent/skills/refact/SKILL.md +139 -0
- package/templates/base/agent/skills/setup-project/SKILL.md +140 -0
- package/templates/base/agent/skills/sync-asana/SKILL.md +106 -0
- package/templates/base/agent/skills/update-canonical-record/SKILL.md +28 -0
- package/templates/base/agent/skills/update-package/SKILL.md +51 -0
- package/templates/base/docs/context/project.md +30 -0
- package/templates/base/docs/decisions.md +22 -0
- package/templates/base/docs/index.md +31 -0
- package/templates/base/docs/sources/raw/.gitkeep +0 -0
- package/templates/base/docs/task/.gitkeep +0 -0
- package/templates/base/env.example +14 -0
- package/templates/base/gitignore +34 -0
- package/templates/overlays/client/agent/skills/create-deliverable/SKILL.md +29 -0
- package/templates/overlays/client/docs/deliverables/.gitkeep +0 -0
- package/templates/overlays/code/agent/skills/add-codebase/SKILL.md +239 -0
- package/templates/overlays/code/agent/skills/code-development/SKILL.md +58 -0
- package/templates/overlays/code/agent/skills/code-development/references/gitflow.md +144 -0
- package/templates/overlays/nextjs/agent/skills/nextjs-dev/SKILL.md +93 -0
- package/templates/overlays/nextjs/agent/skills/setup-netlify-deploy/SKILL.md +143 -0
- package/templates/overlays/nextjs/agent/skills/setup-nextjs-app/SKILL.md +118 -0
- package/templates/overlays/nextjs/agent/skills/setup-vercel-deploy/SKILL.md +116 -0
- package/templates/overlays/wordpress/agent/skills/install-wp-skills/SKILL.md +130 -0
- package/templates/overlays/wordpress/agent/skills/setup-kinsta-deploy/SKILL.md +201 -0
- package/templates/overlays/wordpress/agent/skills/wp-env/SKILL.md +478 -0
- package/templates/overlays/wordpress/wp-cli.yml.example +46 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// .cursor/scripts/sync-asana.mjs
|
|
3
|
+
//
|
|
4
|
+
// Read-only Asana sync: pulls every task in the configured Asana project
|
|
5
|
+
// (open + completed) along with its details and comments, and writes one
|
|
6
|
+
// markdown file per task into `docs/task/open/` (open) or `docs/task/closed/`
|
|
7
|
+
// (completed). Idempotent: re-running updates existing files in place and
|
|
8
|
+
// preserves the `processed:` flag from any prior run.
|
|
9
|
+
//
|
|
10
|
+
// Requires:
|
|
11
|
+
// - .refact-os.json → `asana.projectId` (numeric Asana project GID)
|
|
12
|
+
// - .env → `ASANA_TOKEN` (Asana personal access token)
|
|
13
|
+
//
|
|
14
|
+
// Usage:
|
|
15
|
+
// node .cursor/scripts/sync-asana.mjs # full project sync
|
|
16
|
+
// node .cursor/scripts/sync-asana.mjs --ticket <gid> # sync one task
|
|
17
|
+
// node .cursor/scripts/sync-asana.mjs --dry-run # no file writes
|
|
18
|
+
//
|
|
19
|
+
// This script never writes back to Asana. Creating tickets, adding comments,
|
|
20
|
+
// or any other mutation must remain a deliberate, future change.
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
|
29
|
+
const CONFIG_PATH = path.join(PROJECT_ROOT, ".refact-os.json");
|
|
30
|
+
const ENV_PATH = path.join(PROJECT_ROOT, ".env");
|
|
31
|
+
const ASANA_DIR = path.join(PROJECT_ROOT, "docs", "asana");
|
|
32
|
+
const CLOSED_DIR = path.join(ASANA_DIR, "closed");
|
|
33
|
+
|
|
34
|
+
const ASANA_BASE = process.env.ASANA_API_BASE || "https://app.asana.com/api/1.0";
|
|
35
|
+
const TASK_OPT_FIELDS = [
|
|
36
|
+
"name",
|
|
37
|
+
"notes",
|
|
38
|
+
"completed",
|
|
39
|
+
"completed_at",
|
|
40
|
+
"completed_by.name",
|
|
41
|
+
"completed_by.email",
|
|
42
|
+
"created_at",
|
|
43
|
+
"modified_at",
|
|
44
|
+
"due_on",
|
|
45
|
+
"due_at",
|
|
46
|
+
"start_on",
|
|
47
|
+
"start_at",
|
|
48
|
+
"assignee.name",
|
|
49
|
+
"assignee.email",
|
|
50
|
+
"parent.gid",
|
|
51
|
+
"parent.name",
|
|
52
|
+
"projects.name",
|
|
53
|
+
"memberships.section.name",
|
|
54
|
+
"tags.name",
|
|
55
|
+
"custom_fields.name",
|
|
56
|
+
"custom_fields.display_value",
|
|
57
|
+
"custom_fields.type",
|
|
58
|
+
"num_subtasks",
|
|
59
|
+
"permalink_url",
|
|
60
|
+
"resource_subtype",
|
|
61
|
+
"followers.name",
|
|
62
|
+
].join(",");
|
|
63
|
+
const STORY_OPT_FIELDS = [
|
|
64
|
+
"created_at",
|
|
65
|
+
"created_by.name",
|
|
66
|
+
"created_by.email",
|
|
67
|
+
"text",
|
|
68
|
+
"type",
|
|
69
|
+
"resource_subtype",
|
|
70
|
+
].join(",");
|
|
71
|
+
const SUBTASK_OPT_FIELDS = ["name", "gid", "completed"].join(",");
|
|
72
|
+
const ATTACHMENT_OPT_FIELDS = ["name", "permanent_url", "view_url", "host"].join(",");
|
|
73
|
+
|
|
74
|
+
function parseArgs(argv) {
|
|
75
|
+
const args = { ticket: null, dryRun: false };
|
|
76
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
77
|
+
const a = argv[i];
|
|
78
|
+
if (a === "--ticket" || a === "-t") {
|
|
79
|
+
args.ticket = (argv[i + 1] || "").trim();
|
|
80
|
+
i += 1;
|
|
81
|
+
} else if (a.startsWith("--ticket=")) {
|
|
82
|
+
args.ticket = a.slice("--ticket=".length).trim();
|
|
83
|
+
} else if (a === "--dry-run") {
|
|
84
|
+
args.dryRun = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function loadDotEnv(filePath) {
|
|
91
|
+
if (!existsSync(filePath)) return {};
|
|
92
|
+
const raw = readFileSync(filePath, "utf8");
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
95
|
+
const line = rawLine.trim();
|
|
96
|
+
if (!line || line.startsWith("#")) continue;
|
|
97
|
+
const eq = line.indexOf("=");
|
|
98
|
+
if (eq === -1) continue;
|
|
99
|
+
const key = line.slice(0, eq).trim();
|
|
100
|
+
let value = line.slice(eq + 1).trim();
|
|
101
|
+
if (
|
|
102
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
103
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
104
|
+
) {
|
|
105
|
+
value = value.slice(1, -1);
|
|
106
|
+
}
|
|
107
|
+
out[key] = value;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadConfig() {
|
|
113
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) || {};
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function die(message, code = 1) {
|
|
122
|
+
process.stderr.write(`sync-asana: ${message}\n`);
|
|
123
|
+
process.exit(code);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function asanaFetch(token, urlPath, params = {}) {
|
|
127
|
+
const url = new URL(urlPath.startsWith("http") ? urlPath : `${ASANA_BASE}${urlPath}`);
|
|
128
|
+
for (const [k, v] of Object.entries(params)) {
|
|
129
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
130
|
+
url.searchParams.set(k, v);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let attempt = 0;
|
|
134
|
+
while (true) {
|
|
135
|
+
attempt += 1;
|
|
136
|
+
const res = await fetch(url, {
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${token}`,
|
|
139
|
+
Accept: "application/json",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (res.status === 429 && attempt < 4) {
|
|
143
|
+
const retryAfter = Number(res.headers.get("retry-after") || 2);
|
|
144
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1000));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const body = await res.text().catch(() => "");
|
|
149
|
+
throw new Error(`Asana ${res.status} ${res.statusText} for ${url.pathname}: ${body.slice(0, 200)}`);
|
|
150
|
+
}
|
|
151
|
+
return res.json();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function* paginate(token, urlPath, params) {
|
|
156
|
+
let offset;
|
|
157
|
+
while (true) {
|
|
158
|
+
const page = await asanaFetch(token, urlPath, { ...params, offset });
|
|
159
|
+
for (const item of page.data || []) {
|
|
160
|
+
yield item;
|
|
161
|
+
}
|
|
162
|
+
const next = page.next_page?.offset;
|
|
163
|
+
if (!next) return;
|
|
164
|
+
offset = next;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function fetchProjectTaskGids(token, projectGid) {
|
|
169
|
+
const gids = [];
|
|
170
|
+
for await (const t of paginate(token, `/projects/${projectGid}/tasks`, {
|
|
171
|
+
completed_since: "2000-01-01T00:00:00.000Z",
|
|
172
|
+
limit: 100,
|
|
173
|
+
opt_fields: "gid",
|
|
174
|
+
})) {
|
|
175
|
+
gids.push(t.gid);
|
|
176
|
+
}
|
|
177
|
+
return gids;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function fetchTask(token, gid) {
|
|
181
|
+
const detail = await asanaFetch(token, `/tasks/${gid}`, { opt_fields: TASK_OPT_FIELDS });
|
|
182
|
+
return detail.data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function fetchStories(token, gid) {
|
|
186
|
+
const out = [];
|
|
187
|
+
for await (const s of paginate(token, `/tasks/${gid}/stories`, {
|
|
188
|
+
limit: 100,
|
|
189
|
+
opt_fields: STORY_OPT_FIELDS,
|
|
190
|
+
})) {
|
|
191
|
+
out.push(s);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function fetchSubtasks(token, gid) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for await (const s of paginate(token, `/tasks/${gid}/subtasks`, {
|
|
199
|
+
limit: 100,
|
|
200
|
+
opt_fields: SUBTASK_OPT_FIELDS,
|
|
201
|
+
})) {
|
|
202
|
+
out.push(s);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fetchAttachments(token, gid) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for await (const a of paginate(token, `/tasks/${gid}/attachments`, {
|
|
210
|
+
limit: 100,
|
|
211
|
+
opt_fields: ATTACHMENT_OPT_FIELDS,
|
|
212
|
+
})) {
|
|
213
|
+
out.push(a);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readPreservedProcessed(filePath) {
|
|
219
|
+
if (!existsSync(filePath)) return false;
|
|
220
|
+
const text = readFileSync(filePath, "utf8");
|
|
221
|
+
const m = text.match(/^---[\s\S]*?\nprocessed:\s*(true|false)\s*\n[\s\S]*?---/m);
|
|
222
|
+
return m ? m[1] === "true" : false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function findExistingFile(gid) {
|
|
226
|
+
const open = path.join(ASANA_DIR, `${gid}.md`);
|
|
227
|
+
if (existsSync(open)) return { path: open, dir: "open" };
|
|
228
|
+
const closed = path.join(CLOSED_DIR, `${gid}.md`);
|
|
229
|
+
if (existsSync(closed)) return { path: closed, dir: "closed" };
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderMarkdown(task, stories, subtasks, attachments) {
|
|
234
|
+
const statusLabel = task.completed ? "Completed" : "Open";
|
|
235
|
+
const assignee = task.assignee
|
|
236
|
+
? `${task.assignee.name}${task.assignee.email ? ` (${task.assignee.email})` : ""}`
|
|
237
|
+
: "Unassigned";
|
|
238
|
+
const due = task.due_at || task.due_on || "—";
|
|
239
|
+
const start = task.start_at || task.start_on || "—";
|
|
240
|
+
const section = task.memberships?.[0]?.section?.name;
|
|
241
|
+
const tags = (task.tags || []).map((t) => t.name).filter(Boolean);
|
|
242
|
+
const headerLines = [
|
|
243
|
+
"---",
|
|
244
|
+
"source: asana",
|
|
245
|
+
"added-by: sync-asana.mjs",
|
|
246
|
+
`processed: ${task._preservedProcessed ? "true" : "false"}`,
|
|
247
|
+
`asana-gid: ${task.gid}`,
|
|
248
|
+
`asana-permalink: ${task.permalink_url || ""}`,
|
|
249
|
+
`asana-modified-at: ${task.modified_at || ""}`,
|
|
250
|
+
`asana-completed: ${task.completed ? "true" : "false"}`,
|
|
251
|
+
"---",
|
|
252
|
+
"",
|
|
253
|
+
];
|
|
254
|
+
const lines = [];
|
|
255
|
+
lines.push(`# ${task.name || "(untitled)"}`);
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(
|
|
258
|
+
`**Status:** ${statusLabel} · **Assignee:** ${assignee} · **Due:** ${due} · **Start:** ${start}`,
|
|
259
|
+
);
|
|
260
|
+
if (section) lines.push(`**Section:** ${section}`);
|
|
261
|
+
if (tags.length) lines.push(`**Tags:** ${tags.join(", ")}`);
|
|
262
|
+
if (task.parent?.gid) {
|
|
263
|
+
lines.push(`**Parent task:** ${task.parent.name || ""} (gid: ${task.parent.gid})`);
|
|
264
|
+
}
|
|
265
|
+
if (task.completed_at) {
|
|
266
|
+
const by = task.completed_by?.name ? ` by ${task.completed_by.name}` : "";
|
|
267
|
+
lines.push(`**Completed at:** ${task.completed_at}${by}`);
|
|
268
|
+
}
|
|
269
|
+
lines.push("");
|
|
270
|
+
if (task.notes) {
|
|
271
|
+
lines.push("## Notes");
|
|
272
|
+
lines.push("");
|
|
273
|
+
lines.push(task.notes);
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
const customFields = (task.custom_fields || []).filter((cf) => cf.display_value);
|
|
277
|
+
if (customFields.length) {
|
|
278
|
+
lines.push("## Custom fields");
|
|
279
|
+
lines.push("");
|
|
280
|
+
for (const cf of customFields) {
|
|
281
|
+
lines.push(`- **${cf.name}:** ${cf.display_value}`);
|
|
282
|
+
}
|
|
283
|
+
lines.push("");
|
|
284
|
+
}
|
|
285
|
+
if (subtasks.length) {
|
|
286
|
+
lines.push(`## Subtasks (${subtasks.length})`);
|
|
287
|
+
lines.push("");
|
|
288
|
+
for (const s of subtasks) {
|
|
289
|
+
const box = s.completed ? "[x]" : "[ ]";
|
|
290
|
+
lines.push(`- ${box} ${s.name || "(untitled)"} \`gid:${s.gid}\``);
|
|
291
|
+
}
|
|
292
|
+
lines.push("");
|
|
293
|
+
}
|
|
294
|
+
if (attachments.length) {
|
|
295
|
+
lines.push(`## Attachments (${attachments.length})`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
for (const a of attachments) {
|
|
298
|
+
const url = a.permanent_url || a.view_url || "";
|
|
299
|
+
lines.push(`- [${a.name || "attachment"}](${url})${a.host ? ` _(${a.host})_` : ""}`);
|
|
300
|
+
}
|
|
301
|
+
lines.push("");
|
|
302
|
+
}
|
|
303
|
+
if (stories.length) {
|
|
304
|
+
lines.push(`## Comments / activity (${stories.length})`);
|
|
305
|
+
lines.push("");
|
|
306
|
+
for (const s of stories) {
|
|
307
|
+
const author = s.created_by?.name || "Unknown";
|
|
308
|
+
const subtype = s.resource_subtype || s.type || "";
|
|
309
|
+
lines.push(`### ${s.created_at || ""} — ${author} _(${subtype})_`);
|
|
310
|
+
lines.push("");
|
|
311
|
+
if (s.text) {
|
|
312
|
+
lines.push(s.text);
|
|
313
|
+
lines.push("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return headerLines.join("\n") + lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeTaskFile(task, stories, subtasks, attachments, dryRun) {
|
|
321
|
+
const targetDir = task.completed ? CLOSED_DIR : ASANA_DIR;
|
|
322
|
+
const targetPath = path.join(targetDir, `${task.gid}.md`);
|
|
323
|
+
const existing = findExistingFile(task.gid);
|
|
324
|
+
task._preservedProcessed = existing ? readPreservedProcessed(existing.path) : false;
|
|
325
|
+
const content = renderMarkdown(task, stories, subtasks, attachments);
|
|
326
|
+
|
|
327
|
+
let action = "unchanged";
|
|
328
|
+
if (dryRun) {
|
|
329
|
+
if (!existing) action = "would-create";
|
|
330
|
+
else if (existing.path !== targetPath) action = "would-move";
|
|
331
|
+
else if (readFileSync(existing.path, "utf8") !== content) action = "would-update";
|
|
332
|
+
return { action, targetPath };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
mkdirSync(targetDir, { recursive: true });
|
|
336
|
+
if (existing && existing.path !== targetPath) {
|
|
337
|
+
unlinkSync(existing.path);
|
|
338
|
+
action = "moved";
|
|
339
|
+
} else if (!existing) {
|
|
340
|
+
action = "created";
|
|
341
|
+
} else if (readFileSync(existing.path, "utf8") !== content) {
|
|
342
|
+
action = "updated";
|
|
343
|
+
}
|
|
344
|
+
if (action !== "unchanged") {
|
|
345
|
+
writeFileSync(targetPath, content, "utf8");
|
|
346
|
+
}
|
|
347
|
+
return { action, targetPath };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function summarize(results) {
|
|
351
|
+
const counts = results.reduce((acc, r) => {
|
|
352
|
+
acc[r.action] = (acc[r.action] || 0) + 1;
|
|
353
|
+
return acc;
|
|
354
|
+
}, {});
|
|
355
|
+
return counts;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function main() {
|
|
359
|
+
const args = parseArgs(process.argv.slice(2));
|
|
360
|
+
const env = { ...loadDotEnv(ENV_PATH), ...process.env };
|
|
361
|
+
const token = (env.ASANA_TOKEN || "").trim();
|
|
362
|
+
const config = loadConfig();
|
|
363
|
+
const projectId = config.asana?.projectId;
|
|
364
|
+
|
|
365
|
+
if (!token) {
|
|
366
|
+
die("ASANA_TOKEN is not set. Add it to .env (https://app.asana.com/0/my-apps).");
|
|
367
|
+
}
|
|
368
|
+
if (!args.ticket && (projectId === undefined || projectId === null || projectId === "")) {
|
|
369
|
+
die(
|
|
370
|
+
"asana.projectId is missing in .refact-os.json. Run `npx refact-os-scaffold init` to fill it in, or pass --ticket <gid> to sync a single ticket.",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let taskGids;
|
|
375
|
+
if (args.ticket) {
|
|
376
|
+
taskGids = [args.ticket];
|
|
377
|
+
} else {
|
|
378
|
+
process.stdout.write(`Fetching task list for Asana project ${projectId}…\n`);
|
|
379
|
+
taskGids = await fetchProjectTaskGids(token, projectId);
|
|
380
|
+
process.stdout.write(` found ${taskGids.length} task(s).\n`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const results = [];
|
|
384
|
+
for (const gid of taskGids) {
|
|
385
|
+
try {
|
|
386
|
+
const task = await fetchTask(token, gid);
|
|
387
|
+
const [stories, subtasks, attachments] = await Promise.all([
|
|
388
|
+
fetchStories(token, gid),
|
|
389
|
+
task.num_subtasks > 0 ? fetchSubtasks(token, gid) : Promise.resolve([]),
|
|
390
|
+
fetchAttachments(token, gid),
|
|
391
|
+
]);
|
|
392
|
+
const { action, targetPath } = writeTaskFile(task, stories, subtasks, attachments, args.dryRun);
|
|
393
|
+
results.push({ gid, action, path: targetPath });
|
|
394
|
+
process.stdout.write(` ${action.padEnd(13)} ${path.relative(PROJECT_ROOT, targetPath)}\n`);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
results.push({ gid, action: "error", error: err.message });
|
|
397
|
+
process.stderr.write(` error ${gid}: ${err.message}\n`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const counts = summarize(results);
|
|
402
|
+
process.stdout.write(`\nDone. ${JSON.stringify(counts)}\n`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
main().catch((err) => {
|
|
406
|
+
process.stderr.write(`sync-asana: ${err.message || err}\n`);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adopt
|
|
3
|
+
pattern: procedure
|
|
4
|
+
requires_approval: false
|
|
5
|
+
description: Produce a PLAN for bringing an existing, non-conformant repo up to the agent-first standard. Surveys the repo and outputs a phased transition plan as its final response — it does NOT change anything.
|
|
6
|
+
when_to_use: The user wants to understand what it would take to bring an existing repo to the standard — "adopt this repo", "make this repo agent-first", "what's the plan to conform this repo", "plan the migration". Run after `npx github:refactco/refact-os init --no-force` has laid the mechanical seed.
|
|
7
|
+
when_not_to_use: For a brand-new empty project (use `npx github:refactco/refact-os init`). When the user explicitly asks you to EXECUTE a plan that already exists (this skill only plans — execution is a separate, explicit step the user approves).
|
|
8
|
+
inputs:
|
|
9
|
+
- the target repository (surveyed read-only, not fully read)
|
|
10
|
+
- the standard at docs/agent-first-repo-best-practices.md (or the refact-os package copy)
|
|
11
|
+
- the output of `npm run refact:validate` (read-only)
|
|
12
|
+
outputs:
|
|
13
|
+
- a phased transition plan, presented as the final response. No files are moved, created, or deleted.
|
|
14
|
+
next_skills: []
|
|
15
|
+
sub_agents: []
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Adopt (plan only)
|
|
19
|
+
|
|
20
|
+
Produce a **plan** for bringing an existing repo to the agent-first standard. This skill is **read-only**: it surveys, reasons, and outputs a phased plan as its final response. It does **not** move, create, or delete files, create branches, or run `init`/`migrate`/`sync`. Execution is a separate step the user explicitly approves *after* reading the plan.
|
|
21
|
+
|
|
22
|
+
## Absolute rules
|
|
23
|
+
|
|
24
|
+
- **Change nothing.** No `git mv`, no writes, no deletions, no new branch, no `refact-os init|migrate|sync`. The plan is the only deliverable. If you catch yourself about to edit a file, stop — that's not this skill.
|
|
25
|
+
- **Survey, don't read everything.** Walk the tree; read sizes, first lines, frontmatter, headers; deep-read only what a recommendation needs. For a large repo, fan out one read-only survey sub-agent per top-level area, each returning a short summary. Never try to hold the whole repo at once.
|
|
26
|
+
- **This is the target repo, not refact-os.** You are planning *this* repo's conformance. `refact-os` is the scaffolding tool, not this repo — don't adopt its identity or branding, and don't assume this repo is a refact-os clone. Describe everything in terms of what *this* repo actually is.
|
|
27
|
+
- **Ask only what you must to plan.** If a canonical-vs-duplicate choice blocks the plan, list it as an open question in the plan rather than guessing or acting.
|
|
28
|
+
|
|
29
|
+
## Steps
|
|
30
|
+
|
|
31
|
+
1. **Survey** the repo breadth-first (sub-agents per area). For each area note: what's there, which of the six roles it maps to (Evidence/Knowledge/Task/Output/Software/Agent), and anything load-bearing (scripts, automation, hardcoded paths).
|
|
32
|
+
2. **Read the gaps:** run `npm run refact:validate` (read-only) and fold its findings in. If the script/binary isn't wired up yet, fall back to `npx github:refactco/refact-os validate` (read-only, touches nothing in the repo) — don't run `npm install` here, since this skill changes nothing.
|
|
33
|
+
3. **Write the plan** and present it as your final response. Structure it as ordered **phases**, each a small reviewable unit, with: the proposed moves/renames/**deletions**, the references that would need updating, and any open questions. Typical items:
|
|
34
|
+
- Missing seed → `npx github:refactco/refact-os init --no-force` (additive).
|
|
35
|
+
- Duplicate canonical map (`INDEX.md` vs `docs/index.md`) → which is canonical; merge the other.
|
|
36
|
+
- Duplicate contract (root `AGENTS.md` vs `agent/AGENTS.md`) → consolidate into `agent/AGENTS.md`, leave a thin root pointer.
|
|
37
|
+
- Variant dirs (`docs/tasks` vs `docs/task`) → consolidate onto the standard name.
|
|
38
|
+
- Forbidden `agent/workflows/` / `agent/evals/` → fold each into `agent/skills/<verb-object>/SKILL.md` (orchestrator/review pattern); propose names.
|
|
39
|
+
- **Folders to delete** where a folder is genuinely unneeded (e.g. an empty `docs/deliverables/` on a repo that ships nothing) — list them explicitly so the user can approve.
|
|
40
|
+
- Content not in a clear role → propose a role + destination.
|
|
41
|
+
- **Preserve** `docs/company/` (upstream for `npx refact-os sync company`) and raw evidence — call these out as intentionally unchanged.
|
|
42
|
+
4. **Stop.** End with: "This is a plan only — nothing was changed. Review it; when you're ready, approve the phases you want and I'll execute them one at a time." Do not proceed to execute.
|
|
43
|
+
|
|
44
|
+
## Output shape
|
|
45
|
+
|
|
46
|
+
A short summary, then the phased plan (Phase 1, Phase 2, …) with checkboxes, an "intentionally left unchanged" list, and an "open questions" list. Offer to write the plan to `docs/task/open/<yyyy-mm-dd>-adopt.md` **only if the user asks** — by default, leave the repo untouched.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: close-ticket
|
|
3
|
+
description: Close an open ticket by compressing it to a one-line entry in docs/task/closed/ (or closed.md) with its outcome, and remove the open file.
|
|
4
|
+
pattern: procedure
|
|
5
|
+
when_to_use: A tracked ticket in docs/task/open/ is done or no longer needed.
|
|
6
|
+
when_not_to_use: For opening a ticket (use open-ticket) or for recording a decision (use docs/decisions.md directly).
|
|
7
|
+
inputs:
|
|
8
|
+
- the open ticket file under docs/task/open/
|
|
9
|
+
outputs:
|
|
10
|
+
- a one-line record under docs/task/closed/ and removal of the open file
|
|
11
|
+
next_skills: []
|
|
12
|
+
sub_agents: []
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Close Ticket
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. Read the open ticket in `docs/task/open/<file>.md`.
|
|
20
|
+
2. Append a one-line record to `docs/task/closed/closed.md` (create it if missing):
|
|
21
|
+
```
|
|
22
|
+
- <yyyy-mm-dd> — <title> — <outcome>. (was: <original filename>)
|
|
23
|
+
```
|
|
24
|
+
3. If the full body is worth keeping, move it to `docs/task/closed/<filename>`; otherwise the one-liner is enough.
|
|
25
|
+
4. Delete the file from `docs/task/open/`.
|
|
26
|
+
5. If the work produced a finalized decision, also append to `docs/decisions.md`.
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- Read `closed.md` first to avoid duplicate entries.
|
|
31
|
+
- Keep the one-liner terse — title, date, outcome.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extract-learnings
|
|
3
|
+
description: Capture a durable, generalizable learning — a preference, project convention, workflow/infra fact, or hard-won approach — into docs/context/learnings.md, promoting any hard rule to agent/AGENTS.md.
|
|
4
|
+
pattern: procedure
|
|
5
|
+
when_to_use: A turn revealed something durable — a user preference, a project convention not in agent/AGENTS.md, a generalizable correction, a workflow/infra fact, or a working approach found after trial-and-error. MUST self-check at the end of any multi-step session that established a new file/convention or pinned down setup wiring.
|
|
6
|
+
when_not_to_use: Routine work that revealed nothing new, one-off debugging steps, or facts already in agent/AGENTS.md, docs/decisions.md, or docs/context/learnings.md.
|
|
7
|
+
next_skills: []
|
|
8
|
+
sub_agents: []
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Extract Learnings
|
|
12
|
+
|
|
13
|
+
Write down **non-obvious** things from the current chat — user preferences, project conventions, recurring mistakes, hard-won setup recipes — before they're lost.
|
|
14
|
+
|
|
15
|
+
> **This skill is the project's memory of record, and it takes precedence over personal/global agent memory.** Capture durable *project* facts here, in `docs/context/learnings.md` (and hard rules in `agent/AGENTS.md`) — do **not** also write the same fact to `~/.claude/projects/<dir>/memory/`. The repo is the shared brain; per-user memory is not.
|
|
16
|
+
|
|
17
|
+
## When to invoke
|
|
18
|
+
|
|
19
|
+
Fire when the turn revealed one of these:
|
|
20
|
+
|
|
21
|
+
- Something about the user worth carrying forward — role, expertise, style, environment, tool preferences.
|
|
22
|
+
- An expectation about how the agent should behave.
|
|
23
|
+
- A project convention not already in `agent/AGENTS.md` or `docs/context/`.
|
|
24
|
+
- A correction that generalizes (not a one-off).
|
|
25
|
+
- A concrete workflow fact (branch naming, deploy scripts, review cadence).
|
|
26
|
+
- **A working approach found after trial-and-error.** The winning approach is non-obvious by definition — capture it (plus any gotchas that ruled out wrong paths). Example: "Config changes in `<file>` need a dev-server restart to apply — they aren't hot-reloaded."
|
|
27
|
+
- **Infra wiring that only becomes true after setup** — which command must run after which edit, which port maps to which domain, which env file holds which secrets, which service restart is required for changes to take effect.
|
|
28
|
+
|
|
29
|
+
Skip when:
|
|
30
|
+
|
|
31
|
+
- The turn was routine work and nothing new was revealed.
|
|
32
|
+
- Already covered in `agent/AGENTS.md` § Hard rules, `docs/decisions.md`, or `docs/context/learnings.md`.
|
|
33
|
+
- It was a one-off debugging step with no generalizable rule (one bad commit, one stale cache, one transient network blip).
|
|
34
|
+
- The information is trivially derivable by reading the code, running `git log`, or following an existing doc.
|
|
35
|
+
|
|
36
|
+
Most turns won't trigger this. When unsure, don't fire. **But always self-check at the end of any multi-step session**: *"what did I discover or establish here that the next session would need to know on day one?"* — if the answer is non-empty, capture it. Even a sprawling setup compresses to one bullet that names the key files/paths/commands.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
### 1. Identify the learning
|
|
41
|
+
|
|
42
|
+
Re-read recent turns and ask:
|
|
43
|
+
|
|
44
|
+
- Did I try multiple approaches before one worked? → The winning approach is a learning.
|
|
45
|
+
- Did I create a file with a specific role (override, example, secrets, proxy config)? → That role is a learning.
|
|
46
|
+
- Did I install or configure infra (reverse proxy, DNS, certs, hosts file)? → The wiring is a learning.
|
|
47
|
+
- Did the user push back on, or confirm, an unusual choice? → That feedback is a learning.
|
|
48
|
+
- Did `.gitignore`, environment, or how secrets flow change? → Capture the new convention.
|
|
49
|
+
- Did a command sequence become load-bearing for future setups? → Capture the recipe (one line, naming the entry points).
|
|
50
|
+
|
|
51
|
+
Zero bullets is fine — don't pad. Skip anything already in `docs/context/learnings.md`, `agent/AGENTS.md` § Hard rules, or `docs/decisions.md`.
|
|
52
|
+
|
|
53
|
+
### 2. Append to `docs/context/learnings.md`
|
|
54
|
+
|
|
55
|
+
Under the `## Entries` heading, **newest first**:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
- YYYY-MM-DD — one-line learning.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Use today's date from the current-date system reminder.
|
|
62
|
+
- One bullet, one idea. A complex setup still fits one line — name the key files/paths/commands; the detail lives in the code itself.
|
|
63
|
+
- If it truly cannot be distilled to one line, write it to `docs/context/open-decisions.md` (or a dedicated doc under `docs/`) and tell the user.
|
|
64
|
+
- Append only. **Never** rewrite or reorder existing entries.
|
|
65
|
+
|
|
66
|
+
### 3. Promote Hard rules to `agent/AGENTS.md`
|
|
67
|
+
|
|
68
|
+
If the learning is a hard, non-negotiable rule ("Never X" / "Always X", no edge cases):
|
|
69
|
+
|
|
70
|
+
1. Append as a numbered bullet under `## Hard rules (never)` in `agent/AGENTS.md`. Match the existing terse voice.
|
|
71
|
+
2. Remove that bullet from `docs/context/learnings.md` (the only allowed edit beyond appending, same run only).
|
|
72
|
+
3. List every promoted rule in your report.
|
|
73
|
+
|
|
74
|
+
Bar is **high**. When unsure, leave it in `learnings.md` and flag it. Never promote anything contradicting an existing Hard rule — stop and surface the conflict.
|
|
75
|
+
|
|
76
|
+
### 4. Report
|
|
77
|
+
|
|
78
|
+
One short sentence:
|
|
79
|
+
|
|
80
|
+
- Captured only: `Captured learning: <bullet>.`
|
|
81
|
+
- Captured + promoted: `Captured + promoted to agent/AGENTS.md: <rule>.`
|
|
82
|
+
- Nothing durable: stay silent.
|
|
83
|
+
|
|
84
|
+
## Guardrails
|
|
85
|
+
|
|
86
|
+
- **Never** rewrite or reorder existing entries in `docs/context/learnings.md`. Append-only — the one exception is removing a bullet you just promoted, same run.
|
|
87
|
+
- **Never** delete unrelated entries without the user's say-so.
|
|
88
|
+
- **Never** paste raw chat excerpts. Distill, don't quote.
|
|
89
|
+
- **Never** auto-promote unless certain it's a hard, universal rule.
|
|
90
|
+
- **Never** edit `agent/AGENTS.md` outside `## Hard rules (never)`.
|