@fraylabs/possible 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraylabs/possible",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Create, publish, discover, and reuse source-owned AI Outcomes",
5
5
  "keywords": [
6
6
  "codex",
@@ -23,15 +23,10 @@
23
23
  "bin": {
24
24
  "possible": "src/index.mjs"
25
25
  },
26
- "files": [
27
- "assets/possible",
28
- "src"
29
- ],
26
+ "files": ["src"],
30
27
  "scripts": {
31
- "build": "node scripts/sync-skill.mjs",
32
- "test": "node --test test/*.test.mjs",
33
- "sync:skill": "node scripts/sync-skill.mjs",
34
- "prepack": "node scripts/sync-skill.mjs"
28
+ "build": "node --check src/index.mjs && node --check src/directory.mjs",
29
+ "test": "node --test test/*.test.mjs"
35
30
  },
36
31
  "engines": {
37
32
  "node": ">=22"
@@ -0,0 +1,91 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+
6
+ const DEFAULT_ENDPOINT = "https://reminiscent-lark-333.convex.site/api/outcomes";
7
+
8
+ const directoryEndpoint = () => process.env.POSSIBLE_DIRECTORY_ENDPOINT?.trim() || DEFAULT_ENDPOINT;
9
+
10
+ async function requestDirectory(parameters, fetchImplementation = fetch) {
11
+ const url = new URL(directoryEndpoint());
12
+ for (const [name, value] of Object.entries(parameters)) url.searchParams.set(name, value);
13
+ const response = await fetchImplementation(url, { headers: { accept: "application/json" } });
14
+ const body = await response.json().catch(() => ({}));
15
+ if (!response.ok) throw new Error(body.error || `Possible directory returned HTTP ${response.status}`);
16
+ return body;
17
+ }
18
+
19
+ function installationPath(environment = process.env) {
20
+ const home = environment.POSSIBLE_HOME ? resolve(environment.POSSIBLE_HOME) : join(homedir(), ".possible");
21
+ return join(home, "installation.json");
22
+ }
23
+
24
+ async function installationToken(environment = process.env) {
25
+ const path = installationPath(environment);
26
+ try {
27
+ const stored = JSON.parse(await readFile(path, "utf8"));
28
+ if (stored?.schemaVersion === 1 && typeof stored.id === "string") return stored.id;
29
+ } catch (error) {
30
+ if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) return undefined;
31
+ }
32
+ const id = randomUUID();
33
+ try {
34
+ await mkdir(dirname(path), { recursive: true });
35
+ await writeFile(path, `${JSON.stringify({ schemaVersion: 1, id }, null, 2)}\n`, { flag: "wx" });
36
+ return id;
37
+ } catch (error) {
38
+ if (error?.code === "EEXIST") return installationToken(environment);
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ async function recordUse(outcomeId, { fetchImplementation = fetch, environment = process.env } = {}) {
44
+ if (environment.CI || environment.POSSIBLE_TELEMETRY === "0") return;
45
+ const token = await installationToken(environment);
46
+ if (!token) return;
47
+ const endpoint = new URL(directoryEndpoint());
48
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/, "")}/use`;
49
+ endpoint.search = "";
50
+ try {
51
+ await fetchImplementation(endpoint, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json", accept: "application/json" },
54
+ body: JSON.stringify({
55
+ outcomeId,
56
+ visitorHash: createHash("sha256").update(token).digest("hex"),
57
+ source: "cli",
58
+ ci: false,
59
+ }),
60
+ });
61
+ } catch {
62
+ // Usage reporting never blocks access to a published prompt.
63
+ }
64
+ }
65
+
66
+ export async function searchOutcomes(query, options = {}) {
67
+ const normalized = String(query ?? "").trim();
68
+ if (!normalized) throw new Error("Search requires an ordinary-language query");
69
+ const body = await requestDirectory({ q: normalized, limit: "5" }, options.fetchImplementation);
70
+ return Array.isArray(body.outcomes) ? body.outcomes : [];
71
+ }
72
+
73
+ export async function fetchOutcome(id, options = {}) {
74
+ const normalized = String(id ?? "").trim();
75
+ if (!normalized) throw new Error("Fetch requires an Outcome ID from search results");
76
+ const body = await requestDirectory({ id: normalized }, options.fetchImplementation);
77
+ if (!body.outcome || typeof body.outcome.prompt !== "string") throw new Error("Possible returned an invalid Outcome");
78
+ await recordUse(body.outcome.id, options);
79
+ return body.outcome;
80
+ }
81
+
82
+ export function formatSearchResults(outcomes) {
83
+ if (!outcomes.length) return "No matching Outcomes found.\n";
84
+ return `${outcomes.map((outcome, index) => [
85
+ `${index + 1}. ${outcome.title}`,
86
+ ` ${outcome.summary}`,
87
+ ...(outcome.primary_attribution ? [` Primary: ${outcome.primary_attribution.kind === "product" ? outcome.primary_attribution.id : `${outcome.primary_attribution.repository}/${outcome.primary_attribution.directory}`}`] : []),
88
+ ` ID: ${outcome.id}`,
89
+ ` Source: ${outcome.source_locator}`,
90
+ ].join("\n")).join("\n\n")}\n`;
91
+ }
package/src/index.mjs CHANGED
@@ -2,25 +2,28 @@
2
2
 
3
3
  import process from "node:process";
4
4
  import { runBookmarkCommand } from "./bookmarks.mjs";
5
- import { installPossibleSkill } from "./init.mjs";
5
+ import { fetchOutcome, formatSearchResults, searchOutcomes } from "./directory.mjs";
6
6
  import { addOutcomeSource, createOutcome, publishOutcomeSource, useOutcome, validateOutcomes } from "./outcome-commands.mjs";
7
7
 
8
8
  const HELP = `Possible CLI
9
9
 
10
10
  Usage:
11
- possible init
12
- possible create <slug>
11
+ possible create <slug> --product <owner/product>
12
+ possible create <slug> --skill <owner/repository> <directory> <commit>
13
13
  possible validate [directory]
14
14
  possible publish [owner/repository | https://publisher.example]
15
+ possible search <ordinary-language query>
16
+ possible fetch <outcome-id>
15
17
  possible add <owner/repository | https://publisher.example>
16
18
  possible use <source>@<slug>
17
19
  possible bookmark <command>
18
20
 
19
21
  Commands:
20
- init Install the optional Possible prompt-preparation skill into this project
21
- create Create outcome.json, outcome.md, prompt.md, and media/ for one Outcome
22
+ create Create one Outcome with its required primary Product or Skill
22
23
  validate Validate every Outcome folder below a directory
23
24
  publish Validate and submit one public publisher source; no account required
25
+ search Find relevant Outcomes in the live public directory
26
+ fetch Print one directory Outcome's exact prompt
24
27
  add Discover a public source and save it to .possible/sources.json
25
28
  use Print one exact execution prompt to standard output
26
29
  bookmark add | list | remove locally saved Outcome slugs
@@ -38,9 +41,28 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
38
41
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
39
42
  process.exitCode = 1;
40
43
  }
41
- } else if (args[0] === "create" && args.length === 2) {
44
+ } else if (args[0] === "search" && args.length >= 2) {
42
45
  try {
43
- const folder = await createOutcome(args[1]);
46
+ const outcomes = await searchOutcomes(args.slice(1).join(" "));
47
+ process.stdout.write(formatSearchResults(outcomes));
48
+ } catch (error) {
49
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
50
+ process.exitCode = 1;
51
+ }
52
+ } else if (args[0] === "fetch" && args.length === 2) {
53
+ try {
54
+ const outcome = await fetchOutcome(args[1]);
55
+ process.stdout.write(`${outcome.prompt}\n`);
56
+ } catch (error) {
57
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
58
+ process.exitCode = 1;
59
+ }
60
+ } else if (args[0] === "create" && ((args[2] === "--product" && args.length === 4) || (args[2] === "--skill" && args.length === 6))) {
61
+ try {
62
+ const primary = args[2] === "--product"
63
+ ? { kind: "product", id: args[3] }
64
+ : { kind: "skill", repository: args[3], directory: args[4], lastReviewedCommit: args[5] };
65
+ const folder = await createOutcome(args[1], { primary });
44
66
  process.stdout.write(`Created ${folder}\n`);
45
67
  } catch (error) {
46
68
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -81,15 +103,7 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
81
103
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
82
104
  process.exitCode = 1;
83
105
  }
84
- } else if (args.length !== 1 || args[0] !== "init") {
106
+ } else {
85
107
  process.stderr.write(`Unknown command: ${args.join(" ")}\n\n${HELP}`);
86
108
  process.exitCode = 1;
87
- } else {
88
- try {
89
- const result = await installPossibleSkill();
90
- process.stdout.write(`${result.changed ? "Possible installed" : "Possible is already installed"} at ${result.installPath}\n\nOpen Codex in this project and type:\n\n $possible\n`);
91
- } catch (error) {
92
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
93
- process.exitCode = 1;
94
- }
95
109
  }
@@ -7,10 +7,11 @@ import { discoverOutcomeSource } from "./sources.mjs";
7
7
 
8
8
  const execFileAsync = promisify(execFile);
9
9
  const SAFE_SLUG = /^[a-z0-9][a-z0-9-]*$/;
10
- const DEFAULT_PUBLISH_ENDPOINT = "https://abutwsaahahtbtlopczi.supabase.co/functions/v1/register-outcome-source";
10
+ const DEFAULT_PUBLISH_ENDPOINT = "https://reminiscent-lark-333.convex.site/api/outcomes/register";
11
11
 
12
- export async function createOutcome(slug, { directory = process.cwd() } = {}) {
12
+ export async function createOutcome(slug, { directory = process.cwd(), primary } = {}) {
13
13
  if (!SAFE_SLUG.test(slug ?? "")) throw new Error("Outcome slug must be lowercase and hyphenated");
14
+ if (!primary) throw new Error("Choose one primary attribution with --product or --skill");
14
15
  const root = resolve(directory);
15
16
  const indexPath = join(root, "outcomes.json");
16
17
  let publisherIndex = {
@@ -28,13 +29,14 @@ export async function createOutcome(slug, { directory = process.cwd() } = {}) {
28
29
  await mkdir(join(folder, "media"), { recursive: true });
29
30
  const manifestPath = join(folder, "outcome.json");
30
31
  const manifest = {
31
- schemaVersion: 3,
32
+ schemaVersion: 4,
32
33
  slug,
33
34
  files: { about: "outcome.md", prompt: "prompt.md" },
34
35
  authoredAt: null,
35
36
  author: { name: "Replace with publisher name", url: "https://example.com" },
36
37
  models: [{ provider: "Replace with provider", model: "Replace with model", role: "execution" }],
37
38
  requirements: [],
39
+ primary,
38
40
  };
39
41
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
40
42
  await writeFile(join(folder, "outcome.md"), `# Replace with Outcome name\n\nDescribe the concrete result in one clear opening paragraph.\n`, { flag: "wx" });
@@ -8,7 +8,9 @@ const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
8
8
  const SAFE_REPOSITORY_PATH = /^(?:\.|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/;
9
9
  const MODEL_ROLES = new Set(["authorship", "execution", "review"]);
10
10
  const FILE_TYPES = new Set(["image", "video", "audio", "cad", "document", "data", "source", "archive", "other"]);
11
- const MANIFEST_KEYS = new Set(["schemaVersion", "slug", "files", "authoredAt", "author", "models", "requirements", "products", "skills", "inputs", "artifacts", "preview"]);
11
+ const BASE_MANIFEST_KEYS = ["schemaVersion", "slug", "files", "authoredAt", "author", "models", "requirements", "inputs", "artifacts", "preview"];
12
+ const LEGACY_MANIFEST_KEYS = new Set([...BASE_MANIFEST_KEYS, "products", "skills"]);
13
+ const MANIFEST_KEYS = new Set([...BASE_MANIFEST_KEYS, "primary", "secondary"]);
12
14
 
13
15
  const asObject = (value, context) => {
14
16
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${context} must be a JSON object`);
@@ -56,10 +58,59 @@ const validateFiles = (value, context) => {
56
58
  });
57
59
  };
58
60
 
61
+ const validateSkillReference = (value, context, includeKind = false) => {
62
+ const skill = asObject(value, context);
63
+ exactKeys(skill, new Set(includeKind ? ["kind", "repository", "lastReviewedCommit", "directory"] : ["repository", "lastReviewedCommit", "directory"]), context);
64
+ if (includeKind && skill.kind !== "skill") throw new Error(`${context}.kind must be skill`);
65
+ if (!GITHUB_REPOSITORY.test(string(skill.repository, `${context}.repository`))) throw new Error(`${context}.repository is invalid`);
66
+ if (!EXACT_REVISION.test(string(skill.lastReviewedCommit, `${context}.lastReviewedCommit`))) throw new Error(`${context}.lastReviewedCommit must be an exact commit`);
67
+ const directory = string(skill.directory, `${context}.directory`);
68
+ if (!SAFE_REPOSITORY_PATH.test(directory) || (directory !== "." && directory.split("/").some((part) => part === "." || part === ".."))) throw new Error(`${context}.directory is invalid`);
69
+ return skill;
70
+ };
71
+
72
+ const validateAttribution = (value, context) => {
73
+ const attribution = asObject(value, context);
74
+ if (attribution.kind === "product") {
75
+ exactKeys(attribution, new Set(["kind", "id"]), context);
76
+ if (!PRODUCT_ID.test(string(attribution.id, `${context}.id`))) throw new Error(`${context}.id is invalid`);
77
+ return attribution;
78
+ }
79
+ if (attribution.kind === "skill") return validateSkillReference(attribution, context, true);
80
+ throw new Error(`${context}.kind must be product or skill`);
81
+ };
82
+
83
+ export const attributionKey = (attribution) => attribution.kind === "product"
84
+ ? `product:${attribution.id}`
85
+ : `skill:${attribution.repository}/${attribution.directory}`;
86
+
87
+ export function normalizeOutcomeAttributions(manifest) {
88
+ if (manifest.schemaVersion === 4) {
89
+ const secondary = manifest.secondary ?? [];
90
+ const all = [manifest.primary, ...secondary];
91
+ return {
92
+ primary: manifest.primary,
93
+ secondary,
94
+ products: all.filter((item) => item.kind === "product").map((item) => item.id),
95
+ skills: all.filter((item) => item.kind === "skill").map(({ kind: _kind, ...skill }) => skill),
96
+ };
97
+ }
98
+ const legacy = [
99
+ ...(manifest.products ?? []).map((id) => ({ kind: "product", id })),
100
+ ...(manifest.skills ?? []).map((skill) => ({ kind: "skill", ...skill })),
101
+ ];
102
+ return {
103
+ primary: legacy[0] ?? null,
104
+ secondary: legacy.slice(1),
105
+ products: manifest.products ?? [],
106
+ skills: manifest.skills ?? [],
107
+ };
108
+ }
109
+
59
110
  export function validateOutcomeManifest(value, context = "outcome.json") {
60
111
  const manifest = asObject(value, context);
61
- exactKeys(manifest, MANIFEST_KEYS, context);
62
- if (manifest.schemaVersion !== 3) throw new Error(`${context}.schemaVersion must be 3`);
112
+ if (manifest.schemaVersion !== 3 && manifest.schemaVersion !== 4) throw new Error(`${context}.schemaVersion must be 3 or 4`);
113
+ exactKeys(manifest, manifest.schemaVersion === 4 ? MANIFEST_KEYS : LEGACY_MANIFEST_KEYS, context);
63
114
  if (!SAFE_SLUG.test(string(manifest.slug, `${context}.slug`))) throw new Error(`${context}.slug must be lowercase and hyphenated`);
64
115
 
65
116
  const files = asObject(manifest.files, `${context}.files`);
@@ -92,24 +143,27 @@ export function validateOutcomeManifest(value, context = "outcome.json") {
92
143
  const requirements = manifest.requirements.map((entry, index) => string(entry, `${context}.requirements[${index}]`));
93
144
  if (new Set(requirements).size !== requirements.length) throw new Error(`${context}.requirements contains duplicates`);
94
145
 
95
- if (manifest.products !== undefined) {
146
+ if (manifest.schemaVersion === 3 && manifest.products !== undefined) {
96
147
  if (!Array.isArray(manifest.products) || manifest.products.length === 0) throw new Error(`${context}.products must be omitted or a non-empty array`);
97
148
  for (const [index, product] of manifest.products.entries()) if (!PRODUCT_ID.test(string(product, `${context}.products[${index}]`))) throw new Error(`${context}.products[${index}] is invalid`);
98
149
  if (new Set(manifest.products).size !== manifest.products.length) throw new Error(`${context}.products contains duplicates`);
99
150
  }
100
151
 
101
- if (manifest.skills !== undefined) {
152
+ if (manifest.schemaVersion === 3 && manifest.skills !== undefined) {
102
153
  if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) throw new Error(`${context}.skills must be omitted or a non-empty array`);
103
154
  for (const [index, entry] of manifest.skills.entries()) {
104
- const skill = asObject(entry, `${context}.skills[${index}]`);
105
- exactKeys(skill, new Set(["repository", "lastReviewedCommit", "directory"]), `${context}.skills[${index}]`);
106
- if (!GITHUB_REPOSITORY.test(string(skill.repository, `${context}.skills[${index}].repository`))) throw new Error(`${context}.skills[${index}].repository is invalid`);
107
- if (!EXACT_REVISION.test(string(skill.lastReviewedCommit, `${context}.skills[${index}].lastReviewedCommit`))) throw new Error(`${context}.skills[${index}].lastReviewedCommit must be an exact commit`);
108
- const directory = string(skill.directory, `${context}.skills[${index}].directory`);
109
- if (!SAFE_REPOSITORY_PATH.test(directory) || (directory !== "." && directory.split("/").some((part) => part === "." || part === ".."))) throw new Error(`${context}.skills[${index}].directory is invalid`);
155
+ validateSkillReference(entry, `${context}.skills[${index}]`);
110
156
  }
111
157
  }
112
158
 
159
+ if (manifest.schemaVersion === 4) {
160
+ const primary = validateAttribution(manifest.primary, `${context}.primary`);
161
+ if (manifest.secondary !== undefined && (!Array.isArray(manifest.secondary) || manifest.secondary.length === 0)) throw new Error(`${context}.secondary must be omitted or a non-empty array`);
162
+ const secondary = (manifest.secondary ?? []).map((entry, index) => validateAttribution(entry, `${context}.secondary[${index}]`));
163
+ const keys = [attributionKey(primary), ...secondary.map(attributionKey)];
164
+ if (new Set(keys).size !== keys.length) throw new Error(`${context} contains duplicate primary or secondary attributions`);
165
+ }
166
+
113
167
  validateFiles(manifest.inputs, `${context}.inputs`);
114
168
  validateFiles(manifest.artifacts, `${context}.artifacts`);
115
169
  if (manifest.preview !== undefined) asObject(manifest.preview, `${context}.preview`);
@@ -0,0 +1,36 @@
1
+ export type OutcomeSource = {
2
+ type: "github" | "well-known";
3
+ locator: string;
4
+ installUrl: string;
5
+ };
6
+
7
+ export type DiscoveredOutcome = {
8
+ slug: string;
9
+ title: string;
10
+ summary: string;
11
+ aboutMarkdown: string;
12
+ prompt: string;
13
+ manifest: Record<string, unknown>;
14
+ manifestUrl: string;
15
+ aboutUrl: string;
16
+ promptUrl: string;
17
+ repositoryPath?: string;
18
+ contentHash: string;
19
+ };
20
+
21
+ export type OutcomeDiscovery = OutcomeSource & {
22
+ revision: string;
23
+ publisherName: string;
24
+ outcomes: DiscoveredOutcome[];
25
+ };
26
+
27
+ export type PublicOutcomeSnapshot = {
28
+ schemaVersion: 1;
29
+ source: OutcomeSource & { revision: string };
30
+ publisherName: string;
31
+ outcomes: DiscoveredOutcome[];
32
+ };
33
+
34
+ export function parseOutcomeSource(value: string): OutcomeSource;
35
+ export function discoverOutcomeSource(value: string | OutcomeSource, options?: { githubToken?: string }): Promise<OutcomeDiscovery>;
36
+ export function publicSnapshot(discovery: OutcomeDiscovery): PublicOutcomeSnapshot;
@@ -1,117 +0,0 @@
1
- ---
2
- name: possible
3
- description: Discover proven Outcomes and turn a rough request into one complete execution prompt for a fresh agent. Also author and validate source-owned Outcomes for Possible.
4
- ---
5
-
6
- # Possible
7
-
8
- Possible connects three things:
9
-
10
- - the user's **original prompt**;
11
- - the complete **execution prompt** sent to a working agent;
12
- - the resulting **Outcome**.
13
-
14
- An Outcome is precedent to inspect and remix, not a rigid workflow. Preserve useful creative freedom while giving a fresh agent the context, current methods, and concrete inputs it needs.
15
-
16
- ## Discover before writing
17
-
18
- Preserve the user's original prompt verbatim. Determine whether they want to browse possibilities, prepare a prompt, execute work, or publish completed work.
19
-
20
- For browsing or execution, call `search_outcomes` using ordinary language. Show up to five relevant Outcomes when the user is browsing. When preparing work, fetch the strongest candidates with `fetch_outcome` and read their exact prompts. Search scores indicate text similarity, not quality.
21
-
22
- Use Outcomes as concrete precedent. If none fit, use current primary sources rather than forcing an unrelated example. Prefer official documentation, current source repositories and registries, then reproducible community examples. Check volatile information at run time. Popularity is a discovery signal, not proof that an Outcome is good.
23
-
24
- ## Resolve consequential unknowns
25
-
26
- Collect only what can materially improve execution:
27
-
28
- - the intended result, audience, use, and strong preferences;
29
- - supplied files, measurements, references, credentials, and constraints;
30
- - required inputs that the executor cannot safely invent;
31
- - relevant prior Outcomes and what they actually produced;
32
- - current Products, Skills, tools, environment, permissions, and limits;
33
- - what the user will inspect to decide that the result is finished.
34
-
35
- Ask the fewest questions necessary. Discover safe facts yourself. Infer harmless aesthetic details when a restrained default is sufficient. Do not turn the conversation into a form.
36
-
37
- ## Prepare one execution prompt
38
-
39
- Write one readable, self-contained prompt for a fresh agent. It should naturally state:
40
-
41
- - the exact result and who it is for;
42
- - relevant user context and supplied materials;
43
- - concrete requirements and preferences;
44
- - current Products, Skills, or tools that matter;
45
- - deliverables and where to place them;
46
- - constraints, permissions, and separately authorized external actions;
47
- - what the user will inspect to judge the result;
48
- - unknowns the executor must preserve rather than invent.
49
-
50
- Adapt prior prompts to the current request, date, model, environment, and evidence. Never substitute a summary for a strong full prompt. Never claim an old method is current without checking. Keep the published prompt distinct from the new prompt you prepare.
51
-
52
- Before handoff, confirm that a fresh agent can start without hidden conversation history, missing essential files, consequential unresolved choices, unsupported current claims, or unclear success conditions. Research further or ask one focused question if it cannot.
53
-
54
- Show the proposed execution prompt and name the prior Outcomes and current official sources that materially shaped it.
55
-
56
- ## Hand off once
57
-
58
- After approval, send the execution prompt unchanged to a fresh subagent when that capability is available. Include explicit paths or attachments for every supplied file. One-shot means complete starting context; it does not forbid the executor from inspecting, testing, or repairing its work.
59
-
60
- If fresh subagents are unavailable, return the execution prompt in a copyable block. Do not pretend a handoff occurred.
61
-
62
- Products and Skills describe capabilities. They do not grant permission to spend money, purchase, publish, deploy, contact people, fabricate, operate hardware, or perform another external action.
63
-
64
- ## Author a completed Outcome
65
-
66
- When the user wants to publish completed work, inspect the real result and preserve the exact prompt and provenance. Create one folder:
67
-
68
- ```text
69
- outcomes.json
70
- outcomes/<slug>/
71
- outcome.json
72
- outcome.md
73
- prompt.md
74
- media/ optional
75
- artifacts/ optional
76
- inputs/ optional
77
- ```
78
-
79
- `outcomes.json` is the repository-root publisher index and is created or updated by the CLI. `outcome.md` is the canonical human page: one H1 title, one clear opening summary, and useful formatted explanation. `prompt.md` is the exact reusable execution prompt. `outcome.json` contains machine metadata only: author, authored timestamp, models and agents, required inputs, Products, Skills with last-reviewed commits, and media or artifact references.
80
-
81
- Do not reconstruct absent provenance as fact. Mark unknown values honestly. Products and models belong in `outcome.json`; deliverables and detailed work instructions belong in `prompt.md`; result explanation belongs in `outcome.md`.
82
-
83
- Use the CLI to scaffold and validate:
84
-
85
- ```text
86
- npx @fraylabs/possible@0.2.0 create <slug>
87
- npx @fraylabs/possible@0.2.0 validate [directory]
88
- ```
89
-
90
- ## Publish from the owner's source
91
-
92
- Possible does not host publisher accounts or own the canonical files. A publisher exposes Outcomes from either:
93
-
94
- - a public GitHub repository; or
95
- - `https://publisher.example/.well-known/possible/outcomes.json`.
96
-
97
- The publisher index is a thin list of manifest locations. Possible snapshots the public source revision and displays it; changing the source creates a new revision rather than rewriting history.
98
-
99
- ```text
100
- npx @fraylabs/possible@0.2.0 publish [owner/repository | https://publisher.example]
101
- npx @fraylabs/possible@0.2.0 add <owner/repository | https://publisher.example>
102
- npx @fraylabs/possible@0.2.0 use <source>@<slug>
103
- ```
104
-
105
- GitHub publishing requires a clean committed revision so the snapshot is reproducible. No Possible login is required. A publisher-domain source is Official for that domain; other sources are Community unless their ownership follows directly from the public source.
106
-
107
- ## Local bookmarks
108
-
109
- Use bookmarks only when the user asks:
110
-
111
- ```text
112
- npx @fraylabs/possible@0.2.0 bookmark add <outcome-slug>
113
- npx @fraylabs/possible@0.2.0 bookmark list
114
- npx @fraylabs/possible@0.2.0 bookmark remove <outcome-slug>
115
- ```
116
-
117
- Bookmarks are stored locally in `.possible` and do not require an account.
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Possible"
3
- short_description: "Discover Outcomes and prepare complete prompts"
4
- default_prompt: "Use $possible to find relevant Outcomes and turn my request into a complete execution prompt for a fresh agent."
package/src/init.mjs DELETED
@@ -1,150 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { copyFile, lstat, mkdir, readFile, readdir } from "node:fs/promises";
3
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const DEFAULT_SOURCE = fileURLToPath(new URL("../assets/possible", import.meta.url));
7
- const INSTALL_SEGMENTS = [".agents", "skills", "possible"];
8
-
9
- export class InstallConflictError extends Error {
10
- constructor(conflicts) {
11
- super(
12
- `Possible was not installed because existing files conflict:\n${conflicts
13
- .map((path) => ` - ${path}`)
14
- .join("\n")}\nMove or remove the conflicting path, then run the command again.`,
15
- );
16
- this.name = "InstallConflictError";
17
- this.conflicts = conflicts;
18
- }
19
- }
20
-
21
- const pathExists = async (path) => {
22
- try {
23
- return await lstat(path);
24
- } catch (error) {
25
- if (error?.code === "ENOENT") return null;
26
- throw error;
27
- }
28
- };
29
-
30
- const listTree = async (root, current = "") => {
31
- const directory = join(root, current);
32
- const entries = await readdir(directory, { withFileTypes: true });
33
- const files = [];
34
- const directories = [];
35
-
36
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
37
- const child = join(current, entry.name);
38
- if (entry.isSymbolicLink()) {
39
- throw new Error(`The packaged Possible skill contains an unsupported symbolic link: ${child}`);
40
- }
41
- if (entry.isDirectory()) {
42
- directories.push(child);
43
- const nested = await listTree(root, child);
44
- directories.push(...nested.directories);
45
- files.push(...nested.files);
46
- continue;
47
- }
48
- if (!entry.isFile()) {
49
- throw new Error(`The packaged Possible skill contains an unsupported entry: ${child}`);
50
- }
51
- files.push(child);
52
- }
53
-
54
- return { directories, files };
55
- };
56
-
57
- const sameFile = async (left, right) => {
58
- const [leftContent, rightContent] = await Promise.all([readFile(left), readFile(right)]);
59
- return leftContent.equals(rightContent);
60
- };
61
-
62
- const displayPath = (projectRoot, path) => relative(projectRoot, path) || ".";
63
-
64
- const assertInsideProject = (projectRoot, path) => {
65
- const rel = relative(projectRoot, path);
66
- if (rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) return;
67
- throw new Error(`Refusing to write outside the target project: ${path}`);
68
- };
69
-
70
- const ensureDirectory = async (projectRoot, path) => {
71
- assertInsideProject(projectRoot, path);
72
- try {
73
- await mkdir(path);
74
- } catch (error) {
75
- if (error?.code !== "EEXIST") throw error;
76
- }
77
- const stats = await lstat(path);
78
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
79
- throw new InstallConflictError([displayPath(projectRoot, path)]);
80
- }
81
- };
82
-
83
- export async function installPossibleSkill({ projectDirectory = process.cwd(), sourceDirectory = DEFAULT_SOURCE } = {}) {
84
- const projectRoot = resolve(projectDirectory);
85
- const projectStats = await pathExists(projectRoot);
86
- if (!projectStats?.isDirectory()) {
87
- throw new Error(`Target project is not a directory: ${projectRoot}`);
88
- }
89
-
90
- const sourceRoot = resolve(sourceDirectory);
91
- const sourceStats = await pathExists(sourceRoot);
92
- if (!sourceStats?.isDirectory()) {
93
- throw new Error(`Packaged Possible skill is missing: ${sourceRoot}`);
94
- }
95
-
96
- const tree = await listTree(sourceRoot);
97
- const installRoot = join(projectRoot, ...INSTALL_SEGMENTS);
98
- const directoryPaths = [
99
- join(projectRoot, INSTALL_SEGMENTS[0]),
100
- join(projectRoot, ...INSTALL_SEGMENTS.slice(0, 2)),
101
- installRoot,
102
- ...tree.directories.map((path) => join(installRoot, path)),
103
- ];
104
- const conflicts = [];
105
-
106
- for (const path of directoryPaths) {
107
- assertInsideProject(projectRoot, path);
108
- const stats = await pathExists(path);
109
- if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) {
110
- conflicts.push(displayPath(projectRoot, path));
111
- }
112
- }
113
-
114
- const missingFiles = [];
115
- for (const sourceRelativePath of tree.files) {
116
- const sourcePath = join(sourceRoot, sourceRelativePath);
117
- const destinationPath = join(installRoot, sourceRelativePath);
118
- assertInsideProject(projectRoot, destinationPath);
119
- const stats = await pathExists(destinationPath);
120
- if (!stats) {
121
- missingFiles.push({ sourcePath, destinationPath });
122
- } else if (!stats.isFile() || stats.isSymbolicLink() || !(await sameFile(sourcePath, destinationPath))) {
123
- conflicts.push(displayPath(projectRoot, destinationPath));
124
- }
125
- }
126
-
127
- if (conflicts.length > 0) {
128
- throw new InstallConflictError([...new Set(conflicts)].sort());
129
- }
130
-
131
- for (const path of directoryPaths) {
132
- await ensureDirectory(projectRoot, path);
133
- }
134
- for (const { sourcePath, destinationPath } of missingFiles) {
135
- try {
136
- await copyFile(sourcePath, destinationPath, constants.COPYFILE_EXCL);
137
- } catch (error) {
138
- if (error?.code === "EEXIST") {
139
- throw new InstallConflictError([displayPath(projectRoot, destinationPath)]);
140
- }
141
- throw error;
142
- }
143
- }
144
-
145
- return {
146
- changed: missingFiles.length > 0,
147
- filesWritten: missingFiles.length,
148
- installPath: displayPath(projectRoot, installRoot),
149
- };
150
- }