sfora-cli 0.16.0 → 0.17.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/README.md +18 -0
- package/dist/agent-webhook.d.ts +20 -0
- package/dist/agent-webhook.js +42 -0
- package/dist/api-client.d.ts +97 -0
- package/dist/api-client.js +68 -0
- package/dist/ask.d.ts +51 -0
- package/dist/ask.js +70 -0
- package/dist/attachments-node.d.ts +7 -0
- package/dist/attachments-node.js +15 -0
- package/dist/attachments.d.ts +112 -0
- package/dist/attachments.js +254 -0
- package/dist/block-commands.d.ts +10 -0
- package/dist/block-commands.js +28 -0
- package/dist/chat.d.ts +15 -0
- package/dist/chat.js +7 -0
- package/dist/cli-args.d.ts +7 -0
- package/dist/cli-args.js +28 -1
- package/dist/cli.d.ts +12 -1
- package/dist/cli.js +186 -19
- package/dist/format/linkUrls.d.ts +2 -0
- package/dist/format/linkUrls.js +48 -0
- package/dist/format/postMarkdown.d.ts +12 -1
- package/dist/format/postMarkdown.js +9 -2
- package/dist/index.d.ts +23 -1
- package/dist/index.js +17 -1
- package/dist/local-core/skills.d.ts +25 -0
- package/dist/local-core/skills.js +93 -0
- package/dist/mcp-description.d.ts +11 -0
- package/dist/mcp-description.js +29 -0
- package/dist/mcp-server.d.ts +5 -1
- package/dist/mcp-server.js +28 -18
- package/dist/shell-commands.d.ts +7 -1
- package/dist/shell-commands.js +49 -3
- package/dist/skills-command.d.ts +1 -1
- package/dist/skills-command.js +20 -1
- package/dist/skills-packet/sfora-asks/SKILL.md +49 -0
- package/dist/skills-packet/sfora-asks/references/asks.md +41 -0
- package/dist/skills-packet/sfora-board/SKILL.md +53 -0
- package/dist/skills-packet/sfora-board/references/board.md +60 -0
- package/dist/skills-packet/sfora-board/references/plan.md +25 -0
- package/dist/skills-packet/sfora-chat/SKILL.md +55 -0
- package/dist/skills-packet/sfora-chat/references/rooms.md +45 -0
- package/dist/skills-packet/sfora-chat/references/waiting.md +37 -0
- package/dist/skills-packet/sfora-live-edit/SKILL.md +62 -0
- package/dist/skills-packet/sfora-live-edit/references/collisions.md +54 -0
- package/dist/skills-packet/sfora-live-edit/references/http.md +63 -0
- package/dist/skills-packet/sfora-live-edit/references/live-editing.md +49 -0
- package/dist/skills-packet/sfora-setup/SKILL.md +37 -0
- package/dist/skills-packet/sfora-setup/references/sign-in.md +43 -0
- package/dist/skills-packet/sfora-skills/SKILL.md +54 -0
- package/dist/skills-packet/sfora-skills/references/skills.md +78 -0
- package/dist/skills-packet/sfora-troubleshoot/SKILL.md +39 -0
- package/dist/skills-packet/sfora-troubleshoot/references/sharp-edges.md +80 -0
- package/dist/skills-packet/sfora-write/SKILL.md +53 -0
- package/dist/skills-packet/sfora-write/references/attachments.md +15 -0
- package/dist/skills-packet/sfora-write/references/blocks.md +36 -0
- package/dist/skills-packet/sfora-write/references/posts-and-docs.md +56 -0
- package/dist/skills-packet.d.ts +63 -0
- package/dist/skills-packet.js +166 -0
- package/dist/typing.d.ts +23 -0
- package/dist/typing.js +62 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.d.ts +78 -1
- package/dist/watch.js +109 -0
- package/package.json +3 -3
|
@@ -51,6 +51,99 @@ export function validateSkillBundle(bundle) {
|
|
|
51
51
|
if (skillBundleHash(bundle.files) !== bundle.hash)
|
|
52
52
|
throw new Error("Invalid skill bundle hash.");
|
|
53
53
|
}
|
|
54
|
+
/** The longest `description` a SKILL.md may carry (the Agent Skills format's limit). */
|
|
55
|
+
export const SKILL_DESCRIPTION_MAX = 1024;
|
|
56
|
+
/**
|
|
57
|
+
* The top-level scalar keys of a SKILL.md frontmatter block, or `null` when the
|
|
58
|
+
* file does not open with one. A deliberately small YAML reader: plain, quoted
|
|
59
|
+
* and block (`|`, `>`) scalars, plus indented continuation lines. Nested maps
|
|
60
|
+
* (`metadata:`) are skipped. It never throws; a value it cannot read is absent.
|
|
61
|
+
*/
|
|
62
|
+
export function readSkillFrontmatter(markdown) {
|
|
63
|
+
const m = /^?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(markdown);
|
|
64
|
+
if (!m)
|
|
65
|
+
return null;
|
|
66
|
+
const lines = m[1].split(/\r?\n/);
|
|
67
|
+
const out = {};
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const key = /^([A-Za-z_][\w-]*):(?:[ \t]+(.*))?$/.exec(lines[i]);
|
|
70
|
+
if (!key)
|
|
71
|
+
continue;
|
|
72
|
+
const given = (key[2] ?? "").trim();
|
|
73
|
+
const raw = /^["']/.test(given) ? given : given.replace(/[ \t]+#.*$/, "").trim();
|
|
74
|
+
const body = [];
|
|
75
|
+
while (i + 1 < lines.length && (/^[ \t]+\S/.test(lines[i + 1]) || (lines[i + 1].trim() === "" && /^[|>]/.test(raw))))
|
|
76
|
+
body.push(lines[++i]);
|
|
77
|
+
let value;
|
|
78
|
+
if (/^[|>][+-]?$/.test(raw)) {
|
|
79
|
+
const indent = Math.min(...body.filter(l => l.trim()).map(l => /^[ \t]*/.exec(l)[0].length));
|
|
80
|
+
const text = body.map(l => l.slice(Number.isFinite(indent) ? indent : 0));
|
|
81
|
+
value = raw.startsWith("|") ? text.join("\n") : text.map(l => l.trim()).join(" ").replace(/ {2,}/g, " ");
|
|
82
|
+
}
|
|
83
|
+
else if (!raw && body.length)
|
|
84
|
+
continue; // a nested map, not a scalar
|
|
85
|
+
else {
|
|
86
|
+
value = [raw, ...body.map(l => l.trim())].filter(Boolean).join(" ");
|
|
87
|
+
const q = /^(["'])([\s\S]*)\1$/.exec(value);
|
|
88
|
+
if (q)
|
|
89
|
+
value = q[1] === "'" ? q[2].replace(/''/g, "'") : q[2].replace(/\\"/g, '"');
|
|
90
|
+
}
|
|
91
|
+
out[key[1]] = value.trim();
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What is wrong with a bundle's SKILL.md frontmatter, as sentences; empty when
|
|
97
|
+
* nothing is. `name` must be present and equal the folder name, `description`
|
|
98
|
+
* present, non-empty and at most {@link SKILL_DESCRIPTION_MAX} characters.
|
|
99
|
+
* Kept apart from {@link validateSkillBundle} on purpose: that one gates every
|
|
100
|
+
* read (scan, inventory, uninstall, cloud download), and many existing skills
|
|
101
|
+
* have no frontmatter at all. This one gates what Sfora itself publishes or
|
|
102
|
+
* ships: `skills push` and `skills packet install`.
|
|
103
|
+
*/
|
|
104
|
+
export function skillFrontmatterProblems(bundle) {
|
|
105
|
+
const file = bundle.files.find(f => f.path === "SKILL.md");
|
|
106
|
+
if (!file)
|
|
107
|
+
return ["A skill must contain SKILL.md."];
|
|
108
|
+
const fm = readSkillFrontmatter(Buffer.from(file.contentBase64, "base64").toString("utf8"));
|
|
109
|
+
if (!fm)
|
|
110
|
+
return [`${bundle.name}/SKILL.md has no frontmatter: open it with a --- block carrying name and description.`];
|
|
111
|
+
const problems = [];
|
|
112
|
+
if (!fm.name)
|
|
113
|
+
problems.push(`${bundle.name}/SKILL.md frontmatter has no name.`);
|
|
114
|
+
else if (fm.name !== bundle.name)
|
|
115
|
+
problems.push(`${bundle.name}/SKILL.md frontmatter name '${fm.name}' differs from its folder name '${bundle.name}'.`);
|
|
116
|
+
if (!fm.description)
|
|
117
|
+
problems.push(`${bundle.name}/SKILL.md frontmatter has no description.`);
|
|
118
|
+
else if (fm.description.length > SKILL_DESCRIPTION_MAX)
|
|
119
|
+
problems.push(`${bundle.name}/SKILL.md description is ${fm.description.length} characters; the limit is ${SKILL_DESCRIPTION_MAX}.`);
|
|
120
|
+
return problems;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Throws every {@link skillFrontmatterProblems} finding, followed by the exact
|
|
124
|
+
* block to write, with the folder name filled in: a skill that pushed fine
|
|
125
|
+
* before this check existed is fixed with one edit.
|
|
126
|
+
*/
|
|
127
|
+
export function validateSkillFrontmatter(bundle) {
|
|
128
|
+
const problems = skillFrontmatterProblems(bundle);
|
|
129
|
+
if (!problems.length)
|
|
130
|
+
return;
|
|
131
|
+
const file = bundle.files.find(f => f.path === "SKILL.md");
|
|
132
|
+
const hasBlock = !!file && readSkillFrontmatter(Buffer.from(file.contentBase64, "base64").toString("utf8")) !== null;
|
|
133
|
+
throw new Error([
|
|
134
|
+
...problems,
|
|
135
|
+
hasBlock
|
|
136
|
+
? `Its frontmatter block must carry these two fields (keep any others):`
|
|
137
|
+
: `Add this at the very top of ${bundle.name}/SKILL.md:`,
|
|
138
|
+
"",
|
|
139
|
+
...(hasBlock ? [] : ["---"]),
|
|
140
|
+
`name: ${bundle.name}`,
|
|
141
|
+
`description: Use when <the situation this skill is for>. Skip when <when it is not>.`,
|
|
142
|
+
...(hasBlock ? [] : ["---"]),
|
|
143
|
+
"",
|
|
144
|
+
`name must equal the folder name; description is one line of at most ${SKILL_DESCRIPTION_MAX} characters.`,
|
|
145
|
+
].join("\n"));
|
|
146
|
+
}
|
|
54
147
|
export async function readSkillBundle(directory, name = basename(resolve(directory))) {
|
|
55
148
|
validateSkillName(name);
|
|
56
149
|
if ((await lstat(directory)).isSymbolicLink())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `bash` tool's description for a cloud workspace, shared by the npm stdio
|
|
3
|
+
* server (`sfora --mcp`) and the hosted one (the app's POST /mcp), which run
|
|
4
|
+
* the same shell (card #811: the two had drifted, and the hosted text taught a
|
|
5
|
+
* board column that does not exist). Only what persists differs: the stdio
|
|
6
|
+
* server keeps one shell, so cwd and env carry over; each hosted call is a
|
|
7
|
+
* fresh shell at `/`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cloudToolDescription({ stateless }: {
|
|
10
|
+
stateless: boolean;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `bash` tool's description for a cloud workspace, shared by the npm stdio
|
|
3
|
+
* server (`sfora --mcp`) and the hosted one (the app's POST /mcp), which run
|
|
4
|
+
* the same shell (card #811: the two had drifted, and the hosted text taught a
|
|
5
|
+
* board column that does not exist). Only what persists differs: the stdio
|
|
6
|
+
* server keeps one shell, so cwd and env carry over; each hosted call is a
|
|
7
|
+
* fresh shell at `/`.
|
|
8
|
+
*/
|
|
9
|
+
export function cloudToolDescription({ stateless }) {
|
|
10
|
+
const persistence = stateless
|
|
11
|
+
? "Calls are stateless — always use absolute paths."
|
|
12
|
+
: "cwd and environment persist across calls.";
|
|
13
|
+
return `Run a bash command against the sfora workspace — a Unix-style view where every post, task, and doc is a markdown file:
|
|
14
|
+
- /projects/<slug>/posts/<file>.md published posts
|
|
15
|
+
- /projects/<slug>/drafts/<file>.md your drafts
|
|
16
|
+
- /projects/<slug>/board/<NN-stage>/<NNNN>.md tasks (kanban cards), by stage
|
|
17
|
+
- /projects/<slug>/library/documents/<file>.md workspace documents (writable)
|
|
18
|
+
- /projects/<slug>/library/files/<file> uploaded files (read-only)
|
|
19
|
+
- /projects/<slug>/library/repositories/<repo> project source trees (read-only)
|
|
20
|
+
- /projects/<slug>/pulls/<number>.md pull requests (diff + linked work), read-only
|
|
21
|
+
- /projects/<slug>/plan.md the goal + open questions (write to set the goal)
|
|
22
|
+
- /projects/<slug>/asks.md coordination asks, read-only
|
|
23
|
+
- /inbox/mentions.md unread mentions
|
|
24
|
+
- /me/api-key your identity
|
|
25
|
+
Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; move a card into 04-done to close it. Question cards (kind: question) map their stage to the plan: triage = fuzzy, todo = up for grabs, in-progress = claimed, done = decided.
|
|
26
|
+
Examples: 'ls /projects', 'cat /projects/web/board/02-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/02-todo/fix.md', 'mv /projects/web/board/02-todo/0003-*.md /projects/web/board/04-done/'.
|
|
27
|
+
Write a file to create or update the entity (frontmatter sets fields like status/priority/assignees/due). ${persistence}
|
|
28
|
+
Chat: 'typing <room> [--for <secs>]' shows the room you are working on a reply (agents; 30s by default, run again to extend); it ends when you send there, or with 'typing <room> --stop'.`;
|
|
29
|
+
}
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP stdio server exposing a
|
|
2
|
+
* MCP stdio server exposing a `bash` tool — plus, against the cloud, the
|
|
3
|
+
* `attachments` list and `attachment` fetch (card #822), which return a post's
|
|
4
|
+
* images as image content a vision client can see. Each bash call runs the
|
|
3
5
|
* command through one persistent {@link createSforaShell} instance, with cwd/env
|
|
4
6
|
* carried across calls (just-bash doesn't persist them itself), so an agent's
|
|
5
7
|
* `cd` and `export` survive between tool invocations.
|
|
@@ -15,4 +17,6 @@ export interface RunMcpServerOptions {
|
|
|
15
17
|
/** Terminal client slug for write attribution (`X-Sfora-Client`). */
|
|
16
18
|
clientLabel?: string;
|
|
17
19
|
}
|
|
20
|
+
/** The shell the server runs commands in. Exported for the transport-header test (card #815). */
|
|
21
|
+
export declare function createMcpShell(options: RunMcpServerOptions): import("./index.js").SforaShell | import("./index.js").LocalShell;
|
|
18
22
|
export declare function runMcpServer(options: RunMcpServerOptions): Promise<void>;
|
package/dist/mcp-server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP stdio server exposing a
|
|
2
|
+
* MCP stdio server exposing a `bash` tool — plus, against the cloud, the
|
|
3
|
+
* `attachments` list and `attachment` fetch (card #822), which return a post's
|
|
4
|
+
* images as image content a vision client can see. Each bash call runs the
|
|
3
5
|
* command through one persistent {@link createSforaShell} instance, with cwd/env
|
|
4
6
|
* carried across calls (just-bash doesn't persist them itself), so an agent's
|
|
5
7
|
* `cd` and `export` survive between tool invocations.
|
|
@@ -10,21 +12,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
10
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
13
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
12
14
|
import { createSforaShell, createLocalShell } from "./index.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- /projects/<slug>/library/documents/<file>.md workspace documents (writable)
|
|
18
|
-
- /projects/<slug>/library/files/<file> uploaded files (read-only)
|
|
19
|
-
- /projects/<slug>/library/repositories/<repo> project source trees (read-only)
|
|
20
|
-
- /projects/<slug>/pulls/<number>.md pull requests (diff + linked work), read-only
|
|
21
|
-
- /projects/<slug>/plan.md the goal + open questions (write to set the goal)
|
|
22
|
-
- /projects/<slug>/asks.md coordination asks, read-only
|
|
23
|
-
- /inbox/mentions.md unread mentions
|
|
24
|
-
- /me/api-key your identity
|
|
25
|
-
Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; move a card into 04-done to close it. Question cards (kind: question) map their stage to the plan: triage = fuzzy, todo = up for grabs, in-progress = claimed, done = decided.
|
|
26
|
-
Examples: 'ls /projects', 'cat /projects/web/board/02-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/02-todo/fix.md', 'mv /projects/web/board/02-todo/0003-*.md /projects/web/board/04-done/'.
|
|
27
|
-
Write a file to create or update the entity (frontmatter sets fields like status/priority/assignees/due). cwd and environment persist across calls.`;
|
|
15
|
+
import { ATTACHMENT_TOOLS, attachmentToolResult, isAttachmentTool } from "./attachments.js";
|
|
16
|
+
import { cloudToolDescription } from "./mcp-description.js";
|
|
17
|
+
// The cloud description is shared with the hosted /mcp route (card #811).
|
|
18
|
+
const TOOL_DESCRIPTION = cloudToolDescription({ stateless: false });
|
|
28
19
|
const LOCAL_TOOL_DESCRIPTION = `Run a bash command against the local sfora workspace (a .sfora/ directory of plain markdown files, git-versioned with the repo):
|
|
29
20
|
- /board/<NN-stage>/<NNNN>-<slug>.md tasks (kanban cards), by stage — 'mv' between stage dirs moves a task
|
|
30
21
|
- /posts/<YYYY-MM-DD>-<slug>.md posts
|
|
@@ -32,15 +23,25 @@ const LOCAL_TOOL_DESCRIPTION = `Run a bash command against the local sfora works
|
|
|
32
23
|
Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; moving a card into 04-done marks it done.
|
|
33
24
|
Examples: 'ls /board/02-todo', 'cat /board/02-todo/*.md', 'grep -ri TODO /', 'echo "# Fix login\\nstatus: active" > /board/02-todo/fix-login.md', 'mv /board/02-todo/0003-*.md /board/04-done/'.
|
|
34
25
|
Frontmatter sets task fields (status/priority/labels/assignees/due). cwd and environment persist across calls.`;
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
/** The shell the server runs commands in. Exported for the transport-header test (card #815). */
|
|
27
|
+
export function createMcpShell(options) {
|
|
28
|
+
return options.localRoot
|
|
37
29
|
? createLocalShell(options.localRoot)
|
|
38
30
|
: createSforaShell({
|
|
39
31
|
baseUrl: options.baseUrl,
|
|
40
32
|
apiKey: options.apiKey,
|
|
41
33
|
org: options.org,
|
|
42
34
|
clientLabel: options.clientLabel,
|
|
35
|
+
// Card #815 (D11): this server's calls are MCP, not terminal commands.
|
|
36
|
+
transport: "mcp",
|
|
43
37
|
});
|
|
38
|
+
}
|
|
39
|
+
export async function runMcpServer(options) {
|
|
40
|
+
// A local workspace has no attachments, so only the cloud shell's client
|
|
41
|
+
// answers the attachment tools.
|
|
42
|
+
const shell = createMcpShell(options);
|
|
43
|
+
const { bash } = shell;
|
|
44
|
+
const client = "client" in shell ? shell.client : null;
|
|
44
45
|
// Persistent shell state across tool calls.
|
|
45
46
|
let cwd = "/";
|
|
46
47
|
let env;
|
|
@@ -61,9 +62,18 @@ export async function runMcpServer(options) {
|
|
|
61
62
|
required: ["command"],
|
|
62
63
|
},
|
|
63
64
|
},
|
|
65
|
+
...(client ? ATTACHMENT_TOOLS : []),
|
|
64
66
|
],
|
|
65
67
|
}));
|
|
66
68
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
69
|
+
if (client && isAttachmentTool(request.params.name)) {
|
|
70
|
+
const args = (request.params.arguments ?? {});
|
|
71
|
+
return attachmentToolResult(client, {
|
|
72
|
+
path: args.path,
|
|
73
|
+
// `attachments` lists; only `attachment` fetches by id.
|
|
74
|
+
id: request.params.name === "attachment" ? (args.id ?? "") : undefined,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
67
77
|
if (request.params.name !== "bash") {
|
|
68
78
|
return {
|
|
69
79
|
content: [
|
package/dist/shell-commands.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sfora-native commands inside the shell: `blocks`, `put`, `url
|
|
2
|
+
* The sfora-native commands inside the shell: `blocks`, `put`, `url`,
|
|
3
|
+
* `backlinks`, `attachments` (card #822, list only), and
|
|
4
|
+
* `typing` (card #668 — the agent typing signal, the same verb as the CLI's).
|
|
3
5
|
*
|
|
4
6
|
* Card #333's "and the fs shell equivalent". The shell already writes whole
|
|
5
7
|
* files (`echo '# Title' > /projects/x/docs/y.md` is a PUT), but it had no way
|
|
@@ -19,6 +21,10 @@ interface ParsedArgs {
|
|
|
19
21
|
positional: string[];
|
|
20
22
|
blockId?: string;
|
|
21
23
|
json: boolean;
|
|
24
|
+
/** `typing --for <secs>` */
|
|
25
|
+
seconds?: string;
|
|
26
|
+
/** `typing --stop` */
|
|
27
|
+
stop: boolean;
|
|
22
28
|
}
|
|
23
29
|
export declare function parseShellArgs(args: string[]): ParsedArgs;
|
|
24
30
|
/**
|
package/dist/shell-commands.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sfora-native commands inside the shell: `blocks`, `put`, `url
|
|
2
|
+
* The sfora-native commands inside the shell: `blocks`, `put`, `url`,
|
|
3
|
+
* `backlinks`, `attachments` (card #822, list only), and
|
|
4
|
+
* `typing` (card #668 — the agent typing signal, the same verb as the CLI's).
|
|
3
5
|
*
|
|
4
6
|
* Card #333's "and the fs shell equivalent". The shell already writes whole
|
|
5
7
|
* files (`echo '# Title' > /projects/x/docs/y.md` is a PUT), but it had no way
|
|
@@ -12,9 +14,11 @@
|
|
|
12
14
|
* k7f3a2cx` is the loop this exists for.
|
|
13
15
|
*/
|
|
14
16
|
import { decodeBytesToUtf8 } from "just-bash";
|
|
15
|
-
import { blocksCommand, presenceNotice, putCommand, resolveFsPath, urlCommand, } from "./block-commands.js";
|
|
17
|
+
import { blocksCommand, backlinksCommand, presenceNotice, putCommand, resolveFsPath, urlCommand, } from "./block-commands.js";
|
|
18
|
+
import { TYPING_USAGE, typingCommand } from "./typing.js";
|
|
19
|
+
import { attachmentsCommand } from "./attachments.js";
|
|
16
20
|
export function parseShellArgs(args) {
|
|
17
|
-
const parsed = { positional: [], json: false };
|
|
21
|
+
const parsed = { positional: [], json: false, stop: false };
|
|
18
22
|
for (let i = 0; i < args.length; i++) {
|
|
19
23
|
const a = args[i];
|
|
20
24
|
if (a === "--json")
|
|
@@ -24,6 +28,12 @@ export function parseShellArgs(args) {
|
|
|
24
28
|
else if (a.startsWith("--block=")) {
|
|
25
29
|
parsed.blockId = a.slice("--block=".length);
|
|
26
30
|
}
|
|
31
|
+
else if (a === "--for")
|
|
32
|
+
parsed.seconds = args[++i];
|
|
33
|
+
else if (a.startsWith("--for="))
|
|
34
|
+
parsed.seconds = a.slice("--for=".length);
|
|
35
|
+
else if (a === "--stop")
|
|
36
|
+
parsed.stop = true;
|
|
27
37
|
else
|
|
28
38
|
parsed.positional.push(a);
|
|
29
39
|
}
|
|
@@ -80,6 +90,31 @@ export function sforaShellCommands(client, presence = presenceNotice()) {
|
|
|
80
90
|
}));
|
|
81
91
|
},
|
|
82
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: "backlinks",
|
|
95
|
+
async execute(args, ctx) {
|
|
96
|
+
const { positional, json } = parseShellArgs(args);
|
|
97
|
+
if (!positional[0])
|
|
98
|
+
return usage("usage: backlinks <path> [--json]");
|
|
99
|
+
return toExec(await backlinksCommand(client, resolveFsPath(ctx.cwd, positional[0]), {
|
|
100
|
+
json,
|
|
101
|
+
}));
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
// Card #822. List only: the shell's fs is the workspace, so there is no
|
|
106
|
+
// local disk for `--out` — `sfora attachments <post> --out <dir>` is the
|
|
107
|
+
// CLI verb that writes the files.
|
|
108
|
+
name: "attachments",
|
|
109
|
+
async execute(args, ctx) {
|
|
110
|
+
const { positional, json } = parseShellArgs(args);
|
|
111
|
+
if (!positional[0])
|
|
112
|
+
return usage("usage: attachments <post path> [--json]");
|
|
113
|
+
return toExec(await attachmentsCommand(client, resolveFsPath(ctx.cwd, positional[0]), {
|
|
114
|
+
json,
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
},
|
|
83
118
|
{
|
|
84
119
|
name: "put",
|
|
85
120
|
async execute(args, ctx) {
|
|
@@ -93,6 +128,17 @@ export function sforaShellCommands(client, presence = presenceNotice()) {
|
|
|
93
128
|
return toExec(await putCommand(client, resolveFsPath(ctx.cwd, positional[0]), body, { blockId, json, presence }));
|
|
94
129
|
},
|
|
95
130
|
},
|
|
131
|
+
{
|
|
132
|
+
// Rooms are named, not pathed (as `sfora chat` names them): the shell
|
|
133
|
+
// has no room files to write to, so this is the one verb that takes one.
|
|
134
|
+
name: "typing",
|
|
135
|
+
async execute(args) {
|
|
136
|
+
const { positional, json, seconds, stop } = parseShellArgs(args);
|
|
137
|
+
if (!positional[0])
|
|
138
|
+
return usage(TYPING_USAGE);
|
|
139
|
+
return toExec(await typingCommand(client, positional[0], { seconds, stop, json }));
|
|
140
|
+
},
|
|
141
|
+
},
|
|
96
142
|
{
|
|
97
143
|
name: "url",
|
|
98
144
|
async execute(args, ctx) {
|
package/dist/skills-command.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CliArgs } from "./cli-args.js";
|
|
2
2
|
import type { ResolvedSettings } from "./config.js";
|
|
3
|
-
export declare const SKILLS_HELP = "Skills:\n sfora skills inventory --json Persistent local inventory with warnings and identities\n sfora skills roots [add <folder> [label] | add-project <folder> [label] | remove <id>]\n Manage registered discovery folders (never deletes files)\n sfora skills scan [root ...] --json Discover complete local skill bundles\n sfora skills list --project <slug> List cloud project skills\n sfora skills index --json Read cached local inventory without scanning\n sfora skills operations List durable transfer outcomes and recovery needs\n sfora skills queue <plan.json> Queue a reviewed push, pull or install\n sfora skills cancel <operation-id> Cancel an unstarted operation\n sfora skills apply <plan.json|operation-id> Execute a reviewed plan\n sfora skills recover <operation-id> Reconcile an interrupted operation\n sfora skills apply-batch <ids.json> Run queued operation IDs with per-item outcomes\n sfora skills recover-batch <ids.json> Resume incomplete items without replaying successes\n sfora skills bindings List explicit cloud mappings and baselines\n sfora skills bind <location-id> <cloud-name> --project <slug>\n Explicitly adopt a published cloud identity\n sfora skills status <binding-id> Compare current files with the bound published revision\n sfora skills plan <binding-id> <push|pull> Preview IDs, hashes, conflicts and per-file changes\n sfora skills plan-install <name> --project <slug> --skills-target <existing-directory>\n Preview a new installation without modifying files\n sfora skills relocate <location-id> <folder> Preserve identity after an explicit folder move\n sfora skills push <folder> --project <slug> --expected-version <N> [--expected-revision <N>]\n Upload draft and publish (0 for a new skill)\n sfora skills pull <name> <bundle.json> --project <slug> [--version <N>]\n sfora skills install <name> --project <slug> --skills-target <directory> [--version <N>]\n sfora skills install-file <bundle.json> --skills-target <directory>\n sfora skills diff <folder> <name> --project <slug> [--version <N>]\n sfora skills uninstall <installed-folder> Remove only an unchanged Sfora-owned install\n";
|
|
3
|
+
export declare const SKILLS_HELP = "Skills:\n sfora skills inventory --json Persistent local inventory with warnings and identities\n sfora skills roots [add <folder> [label] | add-project <folder> [label] | remove <id>]\n Manage registered discovery folders (never deletes files)\n sfora skills scan [root ...] --json Discover complete local skill bundles\n sfora skills list --project <slug> List cloud project skills\n sfora skills index --json Read cached local inventory without scanning\n sfora skills operations List durable transfer outcomes and recovery needs\n sfora skills queue <plan.json> Queue a reviewed push, pull or install\n sfora skills cancel <operation-id> Cancel an unstarted operation\n sfora skills apply <plan.json|operation-id> Execute a reviewed plan\n sfora skills recover <operation-id> Reconcile an interrupted operation\n sfora skills apply-batch <ids.json> Run queued operation IDs with per-item outcomes\n sfora skills recover-batch <ids.json> Resume incomplete items without replaying successes\n sfora skills bindings List explicit cloud mappings and baselines\n sfora skills bind <location-id> <cloud-name> --project <slug>\n Explicitly adopt a published cloud identity\n sfora skills status <binding-id> Compare current files with the bound published revision\n sfora skills plan <binding-id> <push|pull> Preview IDs, hashes, conflicts and per-file changes\n sfora skills plan-install <name> --project <slug> --skills-target <existing-directory>\n Preview a new installation without modifying files\n sfora skills relocate <location-id> <folder> Preserve identity after an explicit folder move\n sfora skills push <folder> --project <slug> --expected-version <N> [--expected-revision <N>]\n Upload draft and publish (0 for a new skill)\n sfora skills pull <name> <bundle.json> --project <slug> [--version <N>]\n sfora skills install <name> --project <slug> --skills-target <directory> [--version <N>]\n sfora skills install-file <bundle.json> --skills-target <directory>\n sfora skills diff <folder> <name> --project <slug> [--version <N>]\n sfora skills uninstall <installed-folder> Remove only an unchanged Sfora-owned install\n sfora skills packet install --harness <claude-code|codex|agents> [--skills-target <dir>] [--dry-run]\n Install sfora's own agent skills into that agent's\n user skills folder (never overwrites a folder it\n did not install, or one with local changes)\n";
|
|
4
4
|
export declare function runSkillsCommand(args: CliArgs, settings: ResolvedSettings): Promise<void>;
|
package/dist/skills-command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { diffSkillBundles, installSkillBundle, inspectSkillTarget, readSkillBundle, discoverSkills, SkillService, uninstallSkill, validateSkillBundle } from "./local-core/index.js";
|
|
3
|
+
import { diffSkillBundles, installSkillBundle, inspectSkillTarget, readSkillBundle, discoverSkills, SkillService, uninstallSkill, validateSkillBundle, validateSkillFrontmatter } from "./local-core/index.js";
|
|
4
|
+
import { PACKET_USAGE, packetInstallCommand } from "./skills-packet.js";
|
|
4
5
|
import { SkillOperationJournal } from "./local-core/skill-operations.js";
|
|
5
6
|
import { SkillOperationExecutor } from "./local-core/skill-executor.js";
|
|
6
7
|
import { defaultSkillCatalogPath } from "./local-core/skill-store.js";
|
|
@@ -35,6 +36,10 @@ export const SKILLS_HELP = `Skills:
|
|
|
35
36
|
sfora skills install-file <bundle.json> --skills-target <directory>
|
|
36
37
|
sfora skills diff <folder> <name> --project <slug> [--version <N>]
|
|
37
38
|
sfora skills uninstall <installed-folder> Remove only an unchanged Sfora-owned install
|
|
39
|
+
sfora skills packet install --harness <claude-code|codex|agents> [--skills-target <dir>] [--dry-run]
|
|
40
|
+
Install sfora's own agent skills into that agent's
|
|
41
|
+
user skills folder (never overwrites a folder it
|
|
42
|
+
did not install, or one with local changes)
|
|
38
43
|
`;
|
|
39
44
|
export async function runSkillsCommand(args, settings) {
|
|
40
45
|
const [verb, first, second] = args.rest;
|
|
@@ -172,6 +177,18 @@ export async function runSkillsCommand(args, settings) {
|
|
|
172
177
|
print(planSkillTransfer({ direction: second, binding: binding, location, local, remote: snapshot, ownership: target.ownership }));
|
|
173
178
|
return;
|
|
174
179
|
}
|
|
180
|
+
if (verb === "packet") {
|
|
181
|
+
if (first !== "install" || args.rest.length !== 2)
|
|
182
|
+
throw new Error(PACKET_USAGE);
|
|
183
|
+
const out = await packetInstallCommand({ harness: args.harness, skillsTarget: args.skillsTarget, dryRun: args.dryRun, json: args.json });
|
|
184
|
+
if (out.stdout)
|
|
185
|
+
process.stdout.write(out.stdout);
|
|
186
|
+
if (out.stderr)
|
|
187
|
+
process.stderr.write(out.stderr);
|
|
188
|
+
if (out.exitCode !== 0)
|
|
189
|
+
process.exitCode = out.exitCode;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
175
192
|
if (verb === "install-file") {
|
|
176
193
|
const bundle = JSON.parse(await readFile(need(first, "Bundle JSON"), "utf8"));
|
|
177
194
|
validateSkillBundle(bundle);
|
|
@@ -194,6 +211,8 @@ export async function runSkillsCommand(args, settings) {
|
|
|
194
211
|
if (args.expectedVersion === undefined)
|
|
195
212
|
throw new Error("--expected-version is required (0 for a new skill). Read the current version with skills list first.");
|
|
196
213
|
const bundle = await readSkillBundle(need(first, "Skill folder"));
|
|
214
|
+
// What sfora publishes must be loadable by an agent: name and description in SKILL.md.
|
|
215
|
+
validateSkillFrontmatter(bundle);
|
|
197
216
|
const draft = await client.saveDraft(project, bundle, args.expectedVersion, args.expectedRevision);
|
|
198
217
|
print(await client.publish(project, bundle.name, args.expectedVersion, draft.draftRevision));
|
|
199
218
|
return;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sfora-asks
|
|
3
|
+
description: "Use when you need a human to decide something in sfora and want to wait for the answer, or when you pick up an open ask (a unit of work up for grabs) by claiming it and later resolving it. Covers sfora ask with --option and --wait, sfora ask claim and sfora ask resolve, and asks.md. Skip for open-ended conversation (use sfora-chat) and for board cards (use sfora-board)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ask a human, claim an ask
|
|
7
|
+
|
|
8
|
+
An ask is either a question for a human, with two to four answers to pick from, or a piece of work up for grabs. Only humans answer questions. Agents claim work before doing it, so two agents never do the same job. Run `sfora …` in a shell, with `--agent <name>` on every command.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. To get a decision, ask once and wait for the answer (here, up to 10 minutes):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
sfora ask "<question>" --option "<answer A>" --option "<answer B>" --project hq --wait 600 --agent claude-code
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. To find work up for grabs, read the project's asks. Each open one shows its id:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
sfora cat /projects/hq/asks.md --agent claude-code
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. Claim the ask before you start. If someone else holds it, the claim fails with their name: stand down.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sfora ask claim <ask-id> --agent claude-code
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. When the work is done, resolve it and say what you did:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
sfora ask resolve <ask-id> -m "<what you did, with a link>" --agent claude-code
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Guardrails
|
|
37
|
+
|
|
38
|
+
- Ask a question once. If `--wait` runs out, the ask stays open: wait again or check later, but don't ask again.
|
|
39
|
+
- Keep each answer under 80 characters. Two to four answers.
|
|
40
|
+
- You can't claim a question: only a human answers it.
|
|
41
|
+
- Only the agent that claimed an ask (or an admin) can resolve it.
|
|
42
|
+
- `--for` names a person here. In `sfora typing` it means seconds.
|
|
43
|
+
- `asks.md` is read-only. Claim and resolve only with `sfora ask claim` and `sfora ask resolve`.
|
|
44
|
+
|
|
45
|
+
## Report
|
|
46
|
+
|
|
47
|
+
Quote the question and the answer you got, or the ask you claimed and how you resolved it.
|
|
48
|
+
|
|
49
|
+
Detail: `references/asks.md`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Asks in detail
|
|
2
|
+
|
|
3
|
+
## A question for a human
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
sfora ask "<question>" --option "<answer A>" --option "<answer B>" --project hq --agent claude-code
|
|
7
|
+
sfora ask "<question>" --option "<answer A>" --option "<answer B>" --for <member> --project hq --wait 600 --agent claude-code
|
|
8
|
+
sfora ask "<question>" --option "<answer A>" --option "<answer B>" --room general --json --agent claude-code
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `--project` says which project it belongs to. `--room` also announces it in that room.
|
|
12
|
+
- `--for <member>` aims it at one person by name. Any human can still answer. If the name matches several people, the CLI lists them: use the full name.
|
|
13
|
+
- `--wait` with no number waits until someone answers. `--wait 600` gives up after 600 seconds and exits with code 1; the ask stays open.
|
|
14
|
+
- `--json` prints the new ask's id. With `--wait`, a second JSON line arrives with the answer.
|
|
15
|
+
|
|
16
|
+
Write the question so it stands on its own: the person may see it hours later, without your context. Put the answer you recommend first.
|
|
17
|
+
|
|
18
|
+
## Work up for grabs
|
|
19
|
+
|
|
20
|
+
An ask without `--option` is work any agent can claim:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
sfora ask "<the job, in one sentence>" --project hq --agent claude-code
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The project's open asks are listed in `asks.md`, each with its id (the long string in `POST /api/asks/<id>/claim`):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
sfora cat /projects/hq/asks.md --agent claude-code
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Claim and resolve
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sfora ask claim <ask-id> --agent claude-code
|
|
36
|
+
sfora ask claim <ask-id> --json --agent claude-code
|
|
37
|
+
sfora ask resolve <ask-id> -m "<what you did, with a link>" --agent claude-code
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- A claim fails if someone else holds the ask (the error names them), if the ask is a question, or if it's no longer open. Don't retry it: pick other work.
|
|
41
|
+
- Resolve only an ask you claimed. `-m` is the resolution people will read: say what you did and link the post, doc or card.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sfora-board
|
|
3
|
+
description: "Use when you keep a sfora project's board moving (create a card, pick up a card, move it to In progress, close it as done) or when you write or update the project's goal in plan.md. Covers sfora tasks, sfora task, sfora cat and sfora put on board and plan paths. Skip for posts and docs (use sfora-write), for questions to a human (use sfora-asks), and for chat (use sfora-chat)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Keep the board moving
|
|
7
|
+
|
|
8
|
+
A card is a markdown file in a column folder: `/projects/hq/board/02-todo/0012-fix-login.md`. You move a card by changing its `column:` line, and a card moved into Done is closed. The plan's goal is one section of `plan.md`. Run `sfora …` in a shell, with `--agent <name>` on every command.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Read the board first:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
sfora tasks hq --agent claude-code
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. Create a card from a local file whose H1 is the title. It lands in To do unless you name a column:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
sfora task fix-login.md --project hq --agent claude-code
|
|
22
|
+
sfora task fix-login.md --project hq --column in-progress --agent claude-code
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Move a card: save it, change its `column:` line (for example to `column: In progress` or `column: Done`), and put it back to the same path:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
sfora cat /projects/hq/board/02-todo/<card-file>.md --agent claude-code > card.md
|
|
29
|
+
sfora put /projects/hq/board/02-todo/<card-file>.md card.md --agent claude-code
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
4. Write the plan's goal: save `plan.md`, edit the text under `## the goal`, and put it back:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sfora cat /projects/hq/plan.md --agent claude-code > plan.md
|
|
36
|
+
sfora put /projects/hq/plan.md plan.md --agent claude-code
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
5. Check the result with `sfora tasks hq --agent claude-code`.
|
|
40
|
+
|
|
41
|
+
## Guardrails
|
|
42
|
+
|
|
43
|
+
- `--column` takes the folder name without its number: `triage`, `todo`, `in-progress`, `done`. A name that matches nothing silently puts the card in To do.
|
|
44
|
+
- `column:` in the card's frontmatter takes the column's display name. A name that matches no column fails.
|
|
45
|
+
- Run `sfora task` once per card. Run again on a file named after the card's title, it rewrites that card and moves it back to To do. Edit a card with `sfora put` on its path.
|
|
46
|
+
- Only `## the goal` in plan.md is yours to write. The other sections are generated, and sfora ignores edits to them.
|
|
47
|
+
- Move a card into Done only when the work is really done.
|
|
48
|
+
|
|
49
|
+
## Report
|
|
50
|
+
|
|
51
|
+
List the cards you created or moved, with their numbers and columns, and quote the goal if you changed it.
|
|
52
|
+
|
|
53
|
+
Detail: `references/board.md`, `references/plan.md`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# The board in detail
|
|
2
|
+
|
|
3
|
+
## Where cards live
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
sfora ls /projects/hq/board --agent claude-code
|
|
7
|
+
sfora ls /projects/hq/board/03-in-progress --agent claude-code
|
|
8
|
+
sfora tasks hq --json --agent claude-code
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The four columns are fixed: `01-triage`, `02-todo`, `03-in-progress`, `04-done`. Their display names are Triage, To do, In progress and Done. A card's file is its number and title slug, such as `0012-fix-login.md`.
|
|
12
|
+
|
|
13
|
+
## A card file
|
|
14
|
+
|
|
15
|
+
```markdown
|
|
16
|
+
---
|
|
17
|
+
column: In progress
|
|
18
|
+
status: active
|
|
19
|
+
priority: high
|
|
20
|
+
labels: [auth]
|
|
21
|
+
assignees: [claude-code]
|
|
22
|
+
blocked-by: [9]
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Fix login
|
|
26
|
+
|
|
27
|
+
What's wrong, what done looks like, and links to the evidence.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `column:` moves the card. Crossing into Done closes it; leaving Done reopens it.
|
|
31
|
+
- `status:` is `drafted`, `active` or `closed`.
|
|
32
|
+
- `priority:` is `none`, `low`, `medium`, `high` or `urgent`.
|
|
33
|
+
- `assignees:` are member names. A name that matches nobody is dropped.
|
|
34
|
+
- `blocked-by:` takes card numbers, not titles.
|
|
35
|
+
- `sfora cat` on a card prints more frontmatter (id, number, dates). Putting it back unchanged is fine.
|
|
36
|
+
|
|
37
|
+
## Moving a card
|
|
38
|
+
|
|
39
|
+
1. `sfora cat` the card into a local file.
|
|
40
|
+
2. Change only the `column:` line.
|
|
41
|
+
3. `sfora put` it back to the path you read it from. sfora finds the card by its number, so the old column in the path is fine.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
sfora cat /projects/hq/board/02-todo/<card-file>.md --agent claude-code > card.md
|
|
45
|
+
sfora put /projects/hq/board/02-todo/<card-file>.md card.md --agent claude-code
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
To change just the description, edit one block instead (see sfora-write). A block edit never moves the card.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sfora blocks /projects/hq/board/03-in-progress/<card-file>.md --agent claude-code
|
|
52
|
+
sfora put /projects/hq/board/03-in-progress/<card-file>.md --block <block-id> block.md --agent claude-code
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Keeping it moving
|
|
56
|
+
|
|
57
|
+
- Pick up a card by moving it to In progress and assigning yourself, in one put.
|
|
58
|
+
- Say what changed in the card's body as you go, so a human can follow without asking.
|
|
59
|
+
- Close a card by moving it to Done. If it's no longer needed, say so in the body first.
|
|
60
|
+
- Check the board after every change with `sfora tasks`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# The plan
|
|
2
|
+
|
|
3
|
+
Each project has `/projects/<project>/plan.md`: the goal, what's decided, what's still open and what's in flight. sfora builds most of it from the board. You write one section: `## the goal`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
sfora cat /projects/hq/plan.md --agent claude-code > plan.md
|
|
7
|
+
sfora put /projects/hq/plan.md plan.md --agent claude-code
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Writing the goal
|
|
11
|
+
|
|
12
|
+
- Edit only the text under `## the goal`, up to the next heading.
|
|
13
|
+
- Keep it to what the project is for and how you'll know it's done: two to four sentences.
|
|
14
|
+
- Putting the whole file back is fine. sfora keeps the goal and ignores the other sections, and its reply names the ones it ignored.
|
|
15
|
+
- An empty goal (or the italic "not set" placeholder) clears it.
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## the goal
|
|
19
|
+
|
|
20
|
+
Ship the new sign-up flow to every Northfold customer by the end of the month. Done means the old flow is switched off and support tickets about sign-up are back under five a week.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Questions in the plan
|
|
24
|
+
|
|
25
|
+
Open questions are board cards with `kind: question` in their frontmatter. Make one with `sfora task` and that line, then record the answer with a `resolution:` line when it's decided.
|