@bluewombat/slots 0.1.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.
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { join, relative, resolve } from "node:path";
5
+ import { emitVerdict, GATE_FLAGS, ownArgv } from "@bluewombat/slot-kit";
6
+
7
+ // Parent working files must not change during the Attempt.
8
+ // --parent DIR (repeatable). Other git worktrees of this Child are included.
9
+
10
+ const cwd = resolve(process.cwd());
11
+ const tokens = ownArgv(process.argv, GATE_FLAGS);
12
+ const named = [];
13
+
14
+ for (let i = 0; i < tokens.length; i++) {
15
+ const token = tokens[i];
16
+ if (token === "--parent") {
17
+ const value = tokens[i + 1];
18
+ if (value === undefined || value.startsWith("-")) {
19
+ emitVerdict("fail-blocking", "parent-clean needs a directory after --parent.");
20
+ process.exit(0);
21
+ }
22
+ named.push(resolve(cwd, value));
23
+ i += 1;
24
+ continue;
25
+ }
26
+ emitVerdict("fail-blocking", `parent-clean does not take "${token}". Use --parent <dir>.`);
27
+ process.exit(0);
28
+ }
29
+
30
+ const parents = [...named];
31
+ for (const worktree of otherWorktrees(cwd)) {
32
+ if (!parents.includes(worktree)) {
33
+ parents.push(worktree);
34
+ }
35
+ }
36
+
37
+ if (parents.length === 0) {
38
+ emitVerdict(
39
+ "fail-blocking",
40
+ "parent-clean needs --parent <dir>, or a git worktree Child so it can see the other trees.",
41
+ );
42
+ process.exit(0);
43
+ }
44
+
45
+ const leaked = [];
46
+ for (const parent of parents) {
47
+ if (parent === cwd) {
48
+ continue;
49
+ }
50
+ if (!existsSync(parent) || !statSync(parent).isDirectory()) {
51
+ // A directory that was named on the command line and is not there is a
52
+ // configuration error. A worktree git still lists after its directory went
53
+ // away is git bookkeeping: it holds no working file, so it can leak none.
54
+ if (named.includes(parent)) {
55
+ emitVerdict("fail-blocking", `parent-clean --parent is not a directory: ${parent}`);
56
+ process.exit(0);
57
+ }
58
+ continue;
59
+ }
60
+ leaked.push(...leaksIn(parent, cwd));
61
+ }
62
+
63
+ if (leaked.length > 0) {
64
+ emitVerdict("fail-blocking", `Parent working files changed:\n${leaked.join("\n")}`);
65
+ process.exit(0);
66
+ }
67
+
68
+ emitVerdict("pass");
69
+
70
+ /** Other worktrees of this repository, minus the ones git has marked prunable. */
71
+ /** @param {string} workspace @returns {string[]} */
72
+ function otherWorktrees(workspace) {
73
+ const result = git(workspace, ["worktree", "list", "--porcelain"]);
74
+ if (result.status !== 0) {
75
+ return [];
76
+ }
77
+ const trees = [];
78
+ for (const record of result.stdout.split("\n\n")) {
79
+ const lines = record.split("\n");
80
+ const entry = lines.find((line) => line.startsWith("worktree "));
81
+ if (entry === undefined || lines.some((line) => line.startsWith("prunable"))) {
82
+ continue;
83
+ }
84
+ const path = resolve(entry.slice("worktree ".length));
85
+ if (path !== workspace) {
86
+ trees.push(path);
87
+ }
88
+ }
89
+ return trees;
90
+ }
91
+
92
+ /**
93
+ * @param {string} parent
94
+ * @param {string} workspace
95
+ * @returns {string[]}
96
+ */
97
+ function leaksIn(parent, workspace) {
98
+ if (existsSync(join(parent, ".git"))) {
99
+ const status = git(parent, ["status", "--porcelain", "-uall"]);
100
+ if (status.status !== 0) {
101
+ return [`${parent}: git status failed`];
102
+ }
103
+ const lines = status.stdout
104
+ .split("\n")
105
+ .map((line) => line.trimEnd())
106
+ .filter(Boolean);
107
+ return lines.map((line) => `${relative(workspace, parent) || parent}: ${line}`);
108
+ }
109
+ const referenceMs = referenceTime(workspace);
110
+ /** @type {string[]} */
111
+ const dirty = [];
112
+ walkFiles(parent, (file) => {
113
+ if (statSync(file).mtimeMs > referenceMs) {
114
+ dirty.push(`${relative(workspace, parent) || parent}: ${relative(parent, file)}`);
115
+ }
116
+ });
117
+ return dirty;
118
+ }
119
+
120
+ /** @param {string} workspace */
121
+ function referenceTime(workspace) {
122
+ const gitFile = join(workspace, ".git");
123
+ if (existsSync(gitFile)) {
124
+ return statSync(gitFile).mtimeMs;
125
+ }
126
+ let oldest = Number.POSITIVE_INFINITY;
127
+ walkFiles(workspace, (file) => {
128
+ oldest = Math.min(oldest, statSync(file).mtimeMs);
129
+ });
130
+ if (Number.isFinite(oldest)) {
131
+ return oldest;
132
+ }
133
+ return statSync(workspace).mtimeMs;
134
+ }
135
+
136
+ /**
137
+ * @param {string} dir
138
+ * @param {(file: string) => void} visit
139
+ */
140
+ function walkFiles(dir, visit) {
141
+ let entries;
142
+ try {
143
+ entries = readdirSync(dir, { withFileTypes: true });
144
+ } catch {
145
+ return;
146
+ }
147
+ for (const entry of entries) {
148
+ if (entry.name === ".git") {
149
+ continue;
150
+ }
151
+ const full = join(dir, entry.name);
152
+ if (entry.isDirectory()) {
153
+ walkFiles(full, visit);
154
+ continue;
155
+ }
156
+ if (entry.isFile()) {
157
+ visit(full);
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * @param {string} cwd
164
+ * @param {string[]} args
165
+ */
166
+ function git(cwd, args) {
167
+ return spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
168
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+ import { join, relative } from "node:path";
5
+ import { emitVerdict, GATE_FLAGS, ownArgv, pathMatchesGlob } from "@bluewombat/slot-kit";
6
+
7
+ // Fail-retryable when the workspace changed a path matching a glob.
8
+ // Remaining argv after Implementer's flags are the globs.
9
+
10
+ const cwd = process.cwd();
11
+ const globs = ownArgv(process.argv, GATE_FLAGS).filter((token) => token.length > 0);
12
+
13
+ if (globs.length === 0) {
14
+ emitVerdict("fail-blocking", "sensitive-path needs at least one glob.");
15
+ process.exit(0);
16
+ }
17
+
18
+ if (git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() !== "true") {
19
+ emitVerdict("fail-blocking", "sensitive-path needs a git workspace.");
20
+ process.exit(0);
21
+ }
22
+
23
+ const hit = new Set();
24
+
25
+ walkFiles(cwd, (file) => {
26
+ const posix = relative(cwd, file).replaceAll("\\", "/");
27
+ if (!matchesAny(posix, globs)) {
28
+ return;
29
+ }
30
+ const onDisk = readFileSync(file);
31
+ const atHead = blobAtHead(cwd, posix);
32
+ if (atHead === null || Buffer.compare(onDisk, atHead) !== 0) {
33
+ hit.add(posix);
34
+ }
35
+ });
36
+
37
+ const deleted = git(cwd, ["diff", "--name-only", "--diff-filter=D", "HEAD"]);
38
+ if (deleted.status === 0) {
39
+ for (const file of deleted.stdout.split("\n").filter(Boolean)) {
40
+ if (matchesAny(file.replaceAll("\\", "/"), globs)) {
41
+ hit.add(file);
42
+ }
43
+ }
44
+ }
45
+
46
+ if (hit.size > 0) {
47
+ emitVerdict("fail-retryable", `Sensitive paths changed:\n${[...hit].sort().join("\n")}`);
48
+ process.exit(0);
49
+ }
50
+
51
+ emitVerdict("pass");
52
+
53
+ /**
54
+ * @param {string} path
55
+ * @param {readonly string[]} patterns
56
+ */
57
+ function matchesAny(path, patterns) {
58
+ return patterns.some((pattern) => pathMatchesGlob(path, pattern));
59
+ }
60
+
61
+ /**
62
+ * @param {string} workspace
63
+ * @param {string} path
64
+ */
65
+ function blobAtHead(workspace, path) {
66
+ const result = spawnSync("git", ["-C", workspace, "show", `HEAD:${path}`]);
67
+ if (result.status !== 0) {
68
+ return null;
69
+ }
70
+ return result.stdout;
71
+ }
72
+
73
+ /**
74
+ * @param {string} dir
75
+ * @param {(file: string) => void} visit
76
+ */
77
+ function walkFiles(dir, visit) {
78
+ let entries;
79
+ try {
80
+ entries = readdirSync(dir, { withFileTypes: true });
81
+ } catch {
82
+ return;
83
+ }
84
+ for (const entry of entries) {
85
+ if (entry.name === ".git") {
86
+ continue;
87
+ }
88
+ const full = join(dir, entry.name);
89
+ if (entry.isDirectory()) {
90
+ walkFiles(full, visit);
91
+ continue;
92
+ }
93
+ if (entry.isFile()) {
94
+ visit(full);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @param {string} workspace
101
+ * @param {string[]} args
102
+ */
103
+ function git(workspace, args) {
104
+ return spawnSync("git", ["-C", workspace, ...args], { encoding: "utf8" });
105
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { emitVerdict, GATE_FLAGS, ownArgv, stageOf } from "@bluewombat/slot-kit";
4
+
5
+ // An Attempt that changed nothing did not build anything.
6
+ // Work belongs in the working tree: a commit hides it from this Gate and from
7
+ // the Gates that judge the workspace against HEAD. No option.
8
+ //
9
+ // A unit of work only. An assembly is made of work already validated on its own,
10
+ // so there is a real case where the right thing to do is nothing, and refusing
11
+ // it does not send the producer back to work — it sends it back to find
12
+ // something to change. It obliges: the second attempt edits for the sake of
13
+ // editing. A Gate that cannot tell those apart must not be the one to decide.
14
+
15
+ const cwd = process.cwd();
16
+ const tokens = ownArgv(process.argv, GATE_FLAGS);
17
+
18
+ if (tokens.length > 0) {
19
+ emitVerdict("fail-blocking", `workspace-changed takes no option, got "${tokens[0]}".`);
20
+ process.exit(0);
21
+ }
22
+
23
+ if (git(["rev-parse", "--is-inside-work-tree"]).stdout.trim() !== "true") {
24
+ emitVerdict("fail-blocking", "workspace-changed needs a git workspace.");
25
+ process.exit(0);
26
+ }
27
+
28
+ if (stageOf(process.argv) === "assembly") {
29
+ emitVerdict("pass");
30
+ process.exit(0);
31
+ }
32
+
33
+ const status = git(["status", "--porcelain", "-uall"]);
34
+ if (status.status !== 0) {
35
+ emitVerdict("fail-blocking", `workspace-changed cannot read git status: ${status.stderr.trim()}`);
36
+ process.exit(0);
37
+ }
38
+
39
+ if (status.stdout.trim().length === 0) {
40
+ emitVerdict(
41
+ "fail-retryable",
42
+ [
43
+ "Nothing changed in this directory: this run produced nothing.",
44
+ "Write the work into the working tree here, and do not commit it —",
45
+ "a commit hides it from the checks that follow.",
46
+ ].join(" "),
47
+ );
48
+ process.exit(0);
49
+ }
50
+
51
+ emitVerdict("pass");
52
+
53
+ /** @param {string[]} args */
54
+ function git(args) {
55
+ return spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
56
+ }
package/lib/fill.mjs ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What the shipped templates say, and nothing about how a role runs: the
3
+ * plumbing — parse, load, fill, spawn — is `@bluewombat/slot-kit`.
4
+ */
5
+ import { take } from "@bluewombat/slot-kit";
6
+
7
+ /** @param {string | undefined} raw */
8
+ export function doneWhen(raw) {
9
+ const done = (raw ?? "").trim();
10
+ return done.length > 0 ? `Done when:\n${done}` : "";
11
+ }
12
+
13
+ /** @param {readonly string[]} argv */
14
+ export function attemptOf(argv) {
15
+ const parsed = Number.parseInt(take(argv, "--attempt") ?? "", 10);
16
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
17
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { emitMessage, MESSAGE_FLAGS, ownArgv, take } from "@bluewombat/slot-kit";
3
+
4
+ // The feature's commit in the Conventional Commits shape, from the issue's
5
+ // title and body and with no agent: `<type>: <title>`, the body as written.
6
+ //
7
+ // The type is read off the title's first word — a Project that wants a
8
+ // judgement rather than a rule points `authority.describe` at
9
+ // `messages/git-producer.mjs` with an agent after `--`.
10
+ //
11
+ // --scope NAME put a scope on the subject: `feat(api): …`
12
+ // --type NAME one type for everything, instead of guessing it
13
+
14
+ const SUBJECT_MAX = 72;
15
+ /** @type {[RegExp, string][]} */
16
+ const TYPES = [
17
+ [/^(fix|repair|correct|resolve)\b/i, "fix"],
18
+ [/^(document|describe|write (the )?(readme|docs))\b/i, "docs"],
19
+ [/^(test|cover)\b/i, "test"],
20
+ [/^(refactor|rename|move|extract|simplify|clean)\b/i, "refactor"],
21
+ [/^(bump|upgrade|update (the )?dependenc|configure|set up|setup)\b/i, "chore"],
22
+ ];
23
+
24
+ const own = ownArgv(process.argv, MESSAGE_FLAGS);
25
+ const scope = take(own, "--scope");
26
+ const forcedType = take(own, "--type");
27
+ const title = (take(process.argv, "--title") ?? "").trim();
28
+ const intention = (take(process.argv, "--intention") ?? "").trim();
29
+ if (title.length === 0) {
30
+ process.stderr.write("conventional: no --title.\n");
31
+ process.exit(1);
32
+ }
33
+
34
+ const sentence = firstSentence(title);
35
+ const type = forcedType ?? TYPES.find(([pattern]) => pattern.test(sentence))?.[1] ?? "feat";
36
+ const head = `${type}${scope === undefined ? "" : `(${scope})`}: `;
37
+ const subject = `${head}${fit(lowerFirst(sentence), SUBJECT_MAX - head.length)}`;
38
+ if (intention.length === 0) {
39
+ emitMessage(subject);
40
+ } else {
41
+ emitMessage(subject, intention);
42
+ }
43
+
44
+ /** @param {string} text */
45
+ function firstSentence(text) {
46
+ const oneLine = text.replace(/\s+/g, " ").trim();
47
+ const match = /^(.*?[.!?])(?:\s+(?=[A-Z\p{Lu}])|$)/u.exec(oneLine);
48
+ return (match?.[1] ?? oneLine).replace(/[.\s]+$/, "");
49
+ }
50
+
51
+ /** @param {string} text */
52
+ function lowerFirst(text) {
53
+ // A word that is a name or a symbol keeps its case: `Add \`slugify\``, `README`.
54
+ return /^[A-Z][a-z]/.test(text) ? `${text.slice(0, 1).toLowerCase()}${text.slice(1)}` : text;
55
+ }
56
+
57
+ /**
58
+ * @param {string} text
59
+ * @param {number} max
60
+ */
61
+ function fit(text, max) {
62
+ if (text.length <= max) {
63
+ return text;
64
+ }
65
+ const cut = text.slice(0, max - 1);
66
+ const atWord = cut.lastIndexOf(" ");
67
+ return `${(atWord > max / 2 ? cut.slice(0, atWord) : cut).trimEnd()}…`;
68
+ }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ emitMessage,
7
+ fillPrompt,
8
+ loadPrompt,
9
+ loadRules,
10
+ MESSAGE_FLAGS,
11
+ parseRole,
12
+ spawnFilled,
13
+ take,
14
+ textOption,
15
+ transcriptArgs,
16
+ } from "@bluewombat/slot-kit";
17
+
18
+ // The feature's commit message — and the pull request's description with it
19
+ // — written by an agent that read the diff, under the Project's own rules.
20
+ // Asked once per feature. A role slot like the builders: it fills a template
21
+ // and hands it to the agent after `--`; the agent answers in a file this slot
22
+ // reads back. `--prompt-file` replaces the template, `--rules-file` the rules
23
+ // — that is where a team's commit guideline goes.
24
+
25
+ const NAME = "message producer";
26
+
27
+ const DEFAULT_RULES = `- One subject line, imperative mood, at most 72 characters, no trailing period.
28
+ - Say what the change does for the reader of the history, not how it was made.
29
+ - A body only when the subject cannot carry the why; wrap it at 72.
30
+ - Nothing about tools, agents, or this process.`;
31
+
32
+ const DEFAULT_TEMPLATE = `You are writing the commit message for the change in this directory. It
33
+ is also what the pull request will say, so it is read by people.
34
+
35
+ Read the change first — \`git log {{target}}..HEAD\` for the commits that make
36
+ it up, \`git diff {{target}}...HEAD\` for what they change. Do not modify
37
+ anything.
38
+
39
+ What was asked ({{id}}):
40
+
41
+ {{title}}
42
+
43
+ {{intention}}
44
+
45
+ Rules:
46
+ {{rules}}
47
+
48
+ Write your answer as JSON, and nothing else, to this file:
49
+
50
+ {{out}}
51
+
52
+ It looks like this:
53
+
54
+ { "subject": "…", "body": "…" }
55
+
56
+ \`body\` may be omitted.
57
+ `;
58
+
59
+ const title = (take(process.argv, "--title") ?? "").trim();
60
+ const intention = (take(process.argv, "--intention") ?? "").trim();
61
+ if (title.length === 0) {
62
+ fail("The message producer received no --title.");
63
+ }
64
+
65
+ const parsed = parseRole(process.argv, MESSAGE_FLAGS, NAME);
66
+ if (!parsed.ok) {
67
+ fail(parsed.reason);
68
+ }
69
+ const template = loadPrompt(parsed.options, DEFAULT_TEMPLATE);
70
+ if (!template.ok) {
71
+ fail(template.reason);
72
+ }
73
+ const rules = loadRules(parsed.options, DEFAULT_RULES);
74
+ if (!rules.ok) {
75
+ fail(rules.reason);
76
+ }
77
+
78
+ const scratch = mkdtempSync(join(tmpdir(), "message-"));
79
+ const out = join(scratch, "message.json");
80
+ try {
81
+ const rendered = fillPrompt(
82
+ template.value,
83
+ {
84
+ title,
85
+ intention,
86
+ target: take(process.argv, "--target") ?? "",
87
+ id: take(process.argv, "--id") ?? "",
88
+ rules: rules.value.trim(),
89
+ out,
90
+ },
91
+ ["title", "out"],
92
+ textOption(parsed.options, "promptFile") ?? "built-in template",
93
+ );
94
+ if (!rendered.ok) {
95
+ fail(rendered.reason);
96
+ }
97
+ const spawned = await spawnFilled(
98
+ parsed.runner,
99
+ rendered.prompt,
100
+ transcriptArgs(process.argv, { id: `message:${take(process.argv, "--id") ?? ""}` }),
101
+ );
102
+ if (!spawned.ok) {
103
+ fail(spawned.reason);
104
+ }
105
+ if (spawned.run.error !== undefined) {
106
+ fail(`Could not start the agent at ${spawned.about.bin}: ${spawned.run.error.message}`);
107
+ }
108
+ const answer = readAnswer(out);
109
+ if (answer === undefined || typeof answer.subject !== "string" || answer.subject.trim() === "") {
110
+ fail(`The agent wrote no usable message to ${out}.`);
111
+ }
112
+ emitMessage(
113
+ answer.subject.trim(),
114
+ typeof answer.body === "string" && answer.body.trim().length > 0
115
+ ? answer.body.trim()
116
+ : undefined,
117
+ );
118
+ } finally {
119
+ rmSync(scratch, { recursive: true, force: true });
120
+ }
121
+
122
+ /** @param {string} path @returns {Record<string, unknown> | undefined} */
123
+ function readAnswer(path) {
124
+ let text;
125
+ try {
126
+ text = readFileSync(path, "utf8");
127
+ } catch {
128
+ return undefined;
129
+ }
130
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
131
+ for (const candidate of [fenced?.[1], text]) {
132
+ if (candidate === undefined) {
133
+ continue;
134
+ }
135
+ try {
136
+ const value = JSON.parse(candidate);
137
+ if (value !== null && typeof value === "object") {
138
+ return value;
139
+ }
140
+ } catch {
141
+ // try the next reading
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ /** @param {string} reason @returns {never} */
148
+ function fail(reason) {
149
+ process.stderr.write(`${reason}\n`);
150
+ process.exit(1);
151
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@bluewombat/slots",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Shipped mason slots: Planners, Builders, Gates, publishers, and refreshers a Project names in its config.",
6
+ "keywords": [
7
+ "mason",
8
+ "bluewombat",
9
+ "gate",
10
+ "builder",
11
+ "planner"
12
+ ],
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/tilap/bluewombat/tree/main/packages/plugins/slots#readme",
15
+ "bugs": "https://github.com/tilap/bluewombat/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tilap/bluewombat.git",
19
+ "directory": "packages/plugins/slots"
20
+ },
21
+ "exports": {
22
+ "./planners/*": "./planners/*",
23
+ "./builders/*": "./builders/*",
24
+ "./assembly/*": "./assembly/*",
25
+ "./agents/*": "./agents/*",
26
+ "./gates/*": "./gates/*",
27
+ "./publishers/*": "./publishers/*",
28
+ "./refreshers/*": "./refreshers/*",
29
+ "./messages/*": "./messages/*"
30
+ },
31
+ "files": [
32
+ "planners",
33
+ "builders",
34
+ "assembly",
35
+ "agents",
36
+ "lib",
37
+ "gates",
38
+ "README.md",
39
+ "publishers",
40
+ "refreshers",
41
+ "messages"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "engines": {
47
+ "node": ">=24.20.0"
48
+ },
49
+ "scripts": {
50
+ "lint": "biome check .",
51
+ "format": "biome check --write .",
52
+ "tsc": "tsc --noEmit",
53
+ "test": "tsx --test $(find test -name '*.test.ts')"
54
+ },
55
+ "dependencies": {
56
+ "@bluewombat/slot-kit": "^0.1.0"
57
+ }
58
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bootstrap Planner: one Subtask that is the FeatureStandard itself.
4
+ * FeatureBreakdown appends --intention, --key, --max-units.
5
+ */
6
+ import { emitPlan, emitRefusal, take } from "@bluewombat/slot-kit";
7
+
8
+ const intention = (take(process.argv, "--intention") ?? "").trim();
9
+ if (intention.length === 0) {
10
+ emitRefusal("The FeatureStandard has no intention to turn into a Subtask.");
11
+ process.exit(0);
12
+ }
13
+
14
+ emitPlan([
15
+ {
16
+ id: "A",
17
+ intention,
18
+ definition_of_done: "The intention is realized in the workspace.",
19
+ depends_on: [],
20
+ },
21
+ ]);