sfora-cli 0.15.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 +80 -1
- 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/files.d.ts +1 -1
- package/dist/local-core/files.js +2 -2
- package/dist/local-core/index.d.ts +12 -0
- package/dist/local-core/index.js +11 -0
- package/dist/local-core/skill-adapters.d.ts +21 -0
- package/dist/local-core/skill-adapters.js +19 -0
- package/dist/local-core/skill-discovery.d.ts +22 -0
- package/dist/local-core/skill-discovery.js +79 -0
- package/dist/local-core/skill-domain.d.ts +74 -0
- package/dist/local-core/skill-domain.js +1 -0
- package/dist/local-core/skill-executor.d.ts +23 -0
- package/dist/local-core/skill-executor.js +51 -0
- package/dist/local-core/skill-index.d.ts +54 -0
- package/dist/local-core/skill-index.js +115 -0
- package/dist/local-core/skill-local-executor.d.ts +18 -0
- package/dist/local-core/skill-local-executor.js +249 -0
- package/dist/local-core/skill-operations.d.ts +61 -0
- package/dist/local-core/skill-operations.js +268 -0
- package/dist/local-core/skill-review.d.ts +46 -0
- package/dist/local-core/skill-review.js +132 -0
- package/dist/local-core/skill-service.d.ts +96 -0
- package/dist/local-core/skill-service.js +157 -0
- package/dist/local-core/skill-store.d.ts +34 -0
- package/dist/local-core/skill-store.js +187 -0
- package/dist/local-core/skill-sync.d.ts +132 -0
- package/dist/local-core/skill-sync.js +111 -0
- package/dist/local-core/skills.d.ts +35 -0
- package/dist/local-core/skills.js +142 -37
- 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-client.d.ts +13 -2
- package/dist/skills-client.js +57 -5
- package/dist/skills-command.d.ts +1 -1
- package/dist/skills-command.js +171 -4
- 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 +4 -4
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-client.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type SkillBundle } from "./local-core/skills.js";
|
|
2
2
|
import type { ResolvedSettings } from "./config.js";
|
|
3
|
+
import { type PublishedSkillSnapshot } from "./local-core/skill-sync.js";
|
|
4
|
+
export declare class SkillsApiError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(status: number, body: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class SkillPublicationUnavailableError extends Error {
|
|
9
|
+
readonly code: 'SKILL_NOT_FOUND' | 'SKILL_NOT_PUBLISHED';
|
|
10
|
+
constructor(code: 'SKILL_NOT_FOUND' | 'SKILL_NOT_PUBLISHED', message: string);
|
|
11
|
+
}
|
|
3
12
|
export declare class SkillsClient {
|
|
4
13
|
private readonly settings;
|
|
5
14
|
constructor(settings: ResolvedSettings);
|
|
@@ -13,9 +22,11 @@ export declare class SkillsClient {
|
|
|
13
22
|
[key: string]: unknown;
|
|
14
23
|
}>;
|
|
15
24
|
download(project: string, name: string, version?: number): Promise<SkillBundle>;
|
|
16
|
-
|
|
25
|
+
/** Pin a published revision and verify IDs/hash; lookup labels confer no update authority. */
|
|
26
|
+
snapshot(project: string, name: string): Promise<PublishedSkillSnapshot>;
|
|
27
|
+
saveDraft(project: string, bundle: SkillBundle, expectedVersion: number, expectedRevision?: number, expectedSkillId?: string): Promise<{
|
|
17
28
|
draftRevision: number;
|
|
18
29
|
[key: string]: unknown;
|
|
19
30
|
}>;
|
|
20
|
-
publish(project: string, name: string, expectedVersion: number, expectedRevision?: number): Promise<unknown>;
|
|
31
|
+
publish(project: string, name: string, expectedVersion: number, expectedRevision?: number, expectedSkillId?: string): Promise<unknown>;
|
|
21
32
|
}
|
package/dist/skills-client.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { validateSkillBundle } from "./local-core/skills.js";
|
|
2
|
+
import { SforaApiClient } from "./api-client.js";
|
|
3
|
+
import { validatePublishedSkillSnapshot } from "./local-core/skill-sync.js";
|
|
4
|
+
export class SkillsApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, body) {
|
|
7
|
+
super(`Skills API ${status}: ${body}`);
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class SkillPublicationUnavailableError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
export class SkillsClient {
|
|
3
19
|
settings;
|
|
4
20
|
constructor(settings) {
|
|
@@ -11,7 +27,7 @@ export class SkillsClient {
|
|
|
11
27
|
...init, headers: { Authorization: `Bearer ${this.settings.apiKey}`, "Content-Type": "application/json", "X-Sfora-Client": "cli", ...init?.headers },
|
|
12
28
|
});
|
|
13
29
|
if (!response.ok)
|
|
14
|
-
throw new
|
|
30
|
+
throw new SkillsApiError(response.status, await response.text());
|
|
15
31
|
return await response.json();
|
|
16
32
|
}
|
|
17
33
|
list(project) { return this.request(`/v1/skills?${new URLSearchParams({ project })}`); }
|
|
@@ -26,11 +42,47 @@ export class SkillsClient {
|
|
|
26
42
|
validateSkillBundle(bundle);
|
|
27
43
|
return bundle;
|
|
28
44
|
}
|
|
29
|
-
|
|
45
|
+
/** Pin a published revision and verify IDs/hash; lookup labels confer no update authority. */
|
|
46
|
+
async snapshot(project, name) {
|
|
47
|
+
if (!this.settings.apiKey)
|
|
48
|
+
throw new Error("Run sfora login to connect to cloud Skills.");
|
|
49
|
+
const me = await new SforaApiClient({ baseUrl: this.settings.url, apiKey: this.settings.apiKey }).readMe();
|
|
50
|
+
const accountId = /^memberId: ([a-zA-Z0-9_-]+)$/m.exec(me)?.[1];
|
|
51
|
+
if (!accountId)
|
|
52
|
+
throw new Error('Cloud identity response is missing a member ID.');
|
|
53
|
+
let detail;
|
|
54
|
+
try {
|
|
55
|
+
detail = await this.detail(project, name);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof SkillsApiError && error.status === 404)
|
|
59
|
+
throw new SkillPublicationUnavailableError('SKILL_NOT_FOUND', 'This project has no published skill with that name.');
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const skill = detail.skill;
|
|
63
|
+
if (![skill._id, skill.orgId, skill.projectId].every(v => typeof v === 'string' && v.length > 0))
|
|
64
|
+
throw new Error('Cloud skill response is missing qualified identity fields.');
|
|
65
|
+
if (!Number.isSafeInteger(skill.version) || skill.version < 1)
|
|
66
|
+
throw new SkillPublicationUnavailableError('SKILL_NOT_PUBLISHED', 'This skill has no published version to compare.');
|
|
67
|
+
const versions = detail.versions;
|
|
68
|
+
const revision = Array.isArray(versions) ? versions.find(v => v.version === skill.version && v.skillId === skill._id) : undefined;
|
|
69
|
+
if (!revision || typeof revision.hash !== 'string')
|
|
70
|
+
throw new Error('Cloud skill response is missing the published revision.');
|
|
71
|
+
const bundle = await this.download(project, name, skill.version);
|
|
72
|
+
const snapshot = {
|
|
73
|
+
remote: { deployment: new URL(this.settings.url).origin, organizationId: skill.orgId, projectId: skill.projectId, skillId: skill._id },
|
|
74
|
+
accountId, locator: { project, name }, revision: { hash: revision.hash, cloudVersion: skill.version, contentPolicy: 'bundle-v1' },
|
|
75
|
+
draftRevision: skill.draftRevision, hasDraft: !!skill.draftStorageId, draftHash: skill.draftHash,
|
|
76
|
+
supportsIdentityGuards: Array.isArray(detail.capabilities) && detail.capabilities.includes('qualified-skill-write-v1'), bundle,
|
|
77
|
+
};
|
|
78
|
+
validatePublishedSkillSnapshot(snapshot);
|
|
79
|
+
return snapshot;
|
|
80
|
+
}
|
|
81
|
+
saveDraft(project, bundle, expectedVersion, expectedRevision, expectedSkillId) {
|
|
30
82
|
validateSkillBundle(bundle);
|
|
31
|
-
return this.request("/v1/skills", { method: "POST", body: JSON.stringify({ project, name: bundle.name, bundle, expectedVersion, expectedRevision }) });
|
|
83
|
+
return this.request("/v1/skills", { method: "POST", body: JSON.stringify({ project, name: bundle.name, bundle, expectedVersion, expectedRevision, expectedSkillId }) });
|
|
32
84
|
}
|
|
33
|
-
publish(project, name, expectedVersion, expectedRevision) {
|
|
34
|
-
return this.request("/v1/skills/publish", { method: "POST", body: JSON.stringify({ project, name, expectedVersion, expectedRevision }) });
|
|
85
|
+
publish(project, name, expectedVersion, expectedRevision, expectedSkillId) {
|
|
86
|
+
return this.request("/v1/skills/publish", { method: "POST", body: JSON.stringify({ project, name, expectedVersion, expectedRevision, expectedSkillId }) });
|
|
35
87
|
}
|
|
36
88
|
}
|
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 scan [root ...] --json Discover complete local skill bundles\n sfora skills list --project <slug> List cloud project skills\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,9 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
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";
|
|
5
|
+
import { SkillOperationJournal } from "./local-core/skill-operations.js";
|
|
6
|
+
import { SkillOperationExecutor } from "./local-core/skill-executor.js";
|
|
7
|
+
import { defaultSkillCatalogPath } from "./local-core/skill-store.js";
|
|
3
8
|
import { SkillsClient } from "./skills-client.js";
|
|
9
|
+
import { SkillBindingService, assertSkillBindingSnapshot, compareSkillStates, planSkillTransfer, planSkillInstallation } from './local-core/skill-sync.js';
|
|
4
10
|
export const SKILLS_HELP = `Skills:
|
|
11
|
+
sfora skills inventory --json Persistent local inventory with warnings and identities
|
|
12
|
+
sfora skills roots [add <folder> [label] | add-project <folder> [label] | remove <id>]
|
|
13
|
+
Manage registered discovery folders (never deletes files)
|
|
5
14
|
sfora skills scan [root ...] --json Discover complete local skill bundles
|
|
6
15
|
sfora skills list --project <slug> List cloud project skills
|
|
16
|
+
sfora skills index --json Read cached local inventory without scanning
|
|
17
|
+
sfora skills operations List durable transfer outcomes and recovery needs
|
|
18
|
+
sfora skills queue <plan.json> Queue a reviewed push, pull or install
|
|
19
|
+
sfora skills cancel <operation-id> Cancel an unstarted operation
|
|
20
|
+
sfora skills apply <plan.json|operation-id> Execute a reviewed plan
|
|
21
|
+
sfora skills recover <operation-id> Reconcile an interrupted operation
|
|
22
|
+
sfora skills apply-batch <ids.json> Run queued operation IDs with per-item outcomes
|
|
23
|
+
sfora skills recover-batch <ids.json> Resume incomplete items without replaying successes
|
|
24
|
+
sfora skills bindings List explicit cloud mappings and baselines
|
|
25
|
+
sfora skills bind <location-id> <cloud-name> --project <slug>
|
|
26
|
+
Explicitly adopt a published cloud identity
|
|
27
|
+
sfora skills status <binding-id> Compare current files with the bound published revision
|
|
28
|
+
sfora skills plan <binding-id> <push|pull> Preview IDs, hashes, conflicts and per-file changes
|
|
29
|
+
sfora skills plan-install <name> --project <slug> --skills-target <existing-directory>
|
|
30
|
+
Preview a new installation without modifying files
|
|
31
|
+
sfora skills relocate <location-id> <folder> Preserve identity after an explicit folder move
|
|
7
32
|
sfora skills push <folder> --project <slug> --expected-version <N> [--expected-revision <N>]
|
|
8
33
|
Upload draft and publish (0 for a new skill)
|
|
9
34
|
sfora skills pull <name> <bundle.json> --project <slug> [--version <N>]
|
|
@@ -11,6 +36,10 @@ export const SKILLS_HELP = `Skills:
|
|
|
11
36
|
sfora skills install-file <bundle.json> --skills-target <directory>
|
|
12
37
|
sfora skills diff <folder> <name> --project <slug> [--version <N>]
|
|
13
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)
|
|
14
43
|
`;
|
|
15
44
|
export async function runSkillsCommand(args, settings) {
|
|
16
45
|
const [verb, first, second] = args.rest;
|
|
@@ -21,8 +50,35 @@ export async function runSkillsCommand(args, settings) {
|
|
|
21
50
|
const print = (value) => console.log(JSON.stringify(value, null, args.json ? undefined : 2));
|
|
22
51
|
const need = (value, label) => { if (!value)
|
|
23
52
|
throw new Error(`${label} is required.\n${SKILLS_HELP}`); return value; };
|
|
24
|
-
if (verb === "
|
|
25
|
-
|
|
53
|
+
if (verb === "roots") {
|
|
54
|
+
const service = new SkillService();
|
|
55
|
+
if (!first)
|
|
56
|
+
print(await service.roots());
|
|
57
|
+
else if (first === "add" || first === "add-project")
|
|
58
|
+
print(await service.registerRoot(need(second, "Folder"), args.rest[3], first === "add-project" ? "project" : "custom"));
|
|
59
|
+
else if (first === "remove") {
|
|
60
|
+
await service.removeRoot(need(second, "Root ID"));
|
|
61
|
+
print({ removed: second });
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
throw new Error("Use skills roots, skills roots add <folder> [label], or skills roots remove <id>.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (verb === "index") {
|
|
68
|
+
print(await new SkillService().cachedInventory());
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (verb === "inventory" || verb === "scan") {
|
|
72
|
+
const result = verb === "scan" && args.rest.length > 1
|
|
73
|
+
? await discoverSkills(args.rest.slice(1).map(path => ({ path, label: "Chosen folder", depth: 4 })))
|
|
74
|
+
: await new SkillService().inventory();
|
|
75
|
+
if (verb === "scan") {
|
|
76
|
+
print(result.skills);
|
|
77
|
+
if (result.warnings.length)
|
|
78
|
+
console.error(JSON.stringify({ warnings: result.warnings }));
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
print(result);
|
|
26
82
|
return;
|
|
27
83
|
}
|
|
28
84
|
if (verb === "uninstall") {
|
|
@@ -30,6 +86,109 @@ export async function runSkillsCommand(args, settings) {
|
|
|
30
86
|
print({ uninstalled: first });
|
|
31
87
|
return;
|
|
32
88
|
}
|
|
89
|
+
if (['operations', 'queue', 'cancel', 'apply', 'recover', 'apply-batch', 'recover-batch'].includes(verb)) {
|
|
90
|
+
const journal = new SkillOperationJournal(join(dirname(defaultSkillCatalogPath()), 'operations.sqlite'));
|
|
91
|
+
if (verb === 'operations') {
|
|
92
|
+
print(await journal.list());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (verb === 'cancel') {
|
|
96
|
+
print(await journal.cancel(need(first, 'Operation ID')));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (verb === 'queue') {
|
|
100
|
+
print(await journal.create(JSON.parse(await readFile(need(first, 'Plan JSON'), 'utf8'))));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const service = new SkillService();
|
|
104
|
+
const client = new SkillsClient(settings);
|
|
105
|
+
const executor = new SkillOperationExecutor(service.store, journal, client);
|
|
106
|
+
if (verb === 'apply-batch' || verb === 'recover-batch') {
|
|
107
|
+
const ids = JSON.parse(await readFile(need(first, 'Operation IDs JSON'), 'utf8'));
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const stop = () => { controller.abort(); console.error('Stopping after the current item; unstarted work will be cancelled.'); };
|
|
110
|
+
process.once('SIGINT', stop);
|
|
111
|
+
try {
|
|
112
|
+
const results = await executor.batch(ids, { recover: verb === 'recover-batch', signal: controller.signal });
|
|
113
|
+
print(results);
|
|
114
|
+
if (results.some(r => r.error))
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
process.removeListener('SIGINT', stop);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const operation = verb === 'apply' && !/^[a-f0-9-]{36}$/.test(first ?? '') ? await journal.create(JSON.parse(await readFile(need(first, 'Plan JSON'), 'utf8'))) : undefined;
|
|
123
|
+
const id = operation?.id ?? need(first, 'Operation ID');
|
|
124
|
+
try {
|
|
125
|
+
print(await executor.apply(id, verb === 'recover'));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new Error(`${error.message} Operation ${id} was retained; inspect skills operations, then use skills recover ${id}.`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (['bindings', 'bind', 'status', 'plan', 'relocate'].includes(verb)) {
|
|
133
|
+
const service = new SkillService();
|
|
134
|
+
const bindings = new SkillBindingService(service.store);
|
|
135
|
+
if (verb === 'bindings') {
|
|
136
|
+
print(await bindings.list());
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (verb === 'relocate') {
|
|
140
|
+
print(await service.relocateLocation(need(first, 'Location ID'), need(second, 'Folder')));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const catalog = await service.store.read();
|
|
144
|
+
const binding = verb === 'bind' ? undefined : catalog.bindings.find(b => b.id === need(first, 'Binding ID'));
|
|
145
|
+
if (verb !== 'bind' && !binding)
|
|
146
|
+
throw new Error('Skill binding not found.');
|
|
147
|
+
if (binding && args.project && args.project !== binding.locator.project)
|
|
148
|
+
throw new Error('--project differs from the explicit skill binding.');
|
|
149
|
+
const location = catalog.locations.find(l => verb === 'bind' ? l.id === first : l.skillId === binding.localSkillId);
|
|
150
|
+
if (!location)
|
|
151
|
+
throw new Error('Local skill location not found. Register its folder and refresh the inventory first.');
|
|
152
|
+
if (verb === 'plan' && second !== 'push' && second !== 'pull')
|
|
153
|
+
throw new Error('Choose skills plan <binding-id> <push|pull>.');
|
|
154
|
+
const client = new SkillsClient(settings);
|
|
155
|
+
const snapshot = await client.snapshot(binding?.locator.project ?? need(args.project, '--project'), binding?.locator.name ?? need(second, 'Cloud skill name'));
|
|
156
|
+
if (binding)
|
|
157
|
+
assertSkillBindingSnapshot(binding, snapshot);
|
|
158
|
+
let local;
|
|
159
|
+
try {
|
|
160
|
+
local = { state: 'available', bundle: await readSkillBundle(location.path) };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
local = { state: error.code === 'ENOENT' ? 'missing' : 'unavailable', reason: error.message };
|
|
164
|
+
}
|
|
165
|
+
if (verb === 'bind') {
|
|
166
|
+
if (local.state !== 'available')
|
|
167
|
+
throw new Error(local.reason);
|
|
168
|
+
print(await bindings.adopt(location.skillId, snapshot, local.bundle));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (verb === 'status') {
|
|
172
|
+
print({ bindingId: binding.id, status: compareSkillStates(local, { state: 'available', bundle: snapshot.bundle }, binding.baseline),
|
|
173
|
+
local: local.state === 'available' ? { state: local.state, hash: local.bundle.hash } : local, remote: snapshot.revision, baseline: binding.baseline ?? null });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const target = await inspectSkillTarget(location.path);
|
|
177
|
+
print(planSkillTransfer({ direction: second, binding: binding, location, local, remote: snapshot, ownership: target.ownership }));
|
|
178
|
+
return;
|
|
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
|
+
}
|
|
33
192
|
if (verb === "install-file") {
|
|
34
193
|
const bundle = JSON.parse(await readFile(need(first, "Bundle JSON"), "utf8"));
|
|
35
194
|
validateSkillBundle(bundle);
|
|
@@ -38,6 +197,12 @@ export async function runSkillsCommand(args, settings) {
|
|
|
38
197
|
}
|
|
39
198
|
const project = need(args.project, "--project");
|
|
40
199
|
const client = new SkillsClient(settings);
|
|
200
|
+
if (verb === 'plan-install') {
|
|
201
|
+
const remote = await client.snapshot(project, need(first, 'Skill name'));
|
|
202
|
+
const target = join(await realpath(need(args.skillsTarget, '--skills-target')), remote.bundle.name);
|
|
203
|
+
print(planSkillInstallation(remote, target, await inspectSkillTarget(target)));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
41
206
|
if (verb === "list") {
|
|
42
207
|
print(await client.list(project));
|
|
43
208
|
return;
|
|
@@ -46,6 +211,8 @@ export async function runSkillsCommand(args, settings) {
|
|
|
46
211
|
if (args.expectedVersion === undefined)
|
|
47
212
|
throw new Error("--expected-version is required (0 for a new skill). Read the current version with skills list first.");
|
|
48
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);
|
|
49
216
|
const draft = await client.saveDraft(project, bundle, args.expectedVersion, args.expectedRevision);
|
|
50
217
|
print(await client.publish(project, bundle.name, args.expectedVersion, draft.draftRevision));
|
|
51
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`.
|