thachvd-kit 1.0.38 → 1.0.39

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.
@@ -1,52 +1,52 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { runGit, workspaceForPlan } = require('./sdd-workspace-lib');
6
-
7
- function gitStatus(args) {
8
- const { spawnSync } = require('child_process');
9
- return spawnSync('git', args, { encoding: 'utf8' });
10
- }
11
-
12
- function main() {
13
- if (process.argv.length < 5 || process.argv.length > 6) {
14
- throw new Error('usage: review-package PLAN_FILE BASE HEAD [OUTFILE]');
15
- }
16
- const [, , planFile, base, head, explicitOutput] = process.argv;
17
- if (!fs.existsSync(path.resolve(planFile))) throw new Error(`no such plan file: ${planFile}`);
18
- for (const ref of [base, head]) {
19
- const result = gitStatus(['rev-parse', '--verify', '--quiet', ref]);
20
- if (result.status !== 0) throw new Error(`bad ${ref === base ? 'BASE' : 'HEAD'}: ${ref}`);
21
- }
22
- const ancestor = gitStatus(['merge-base', '--is-ancestor', base, head]);
23
- if (ancestor.status !== 0) throw new Error(`HEAD is not a descendant of BASE: ${base}..${head}`);
24
- const commits = Number(runGit(['rev-list', '--count', `${base}..${head}`]));
25
- if (commits <= 0) throw new Error(`empty commit range: ${base}..${head}`);
26
-
27
- const outputPath = explicitOutput || path.join(
28
- workspaceForPlan(planFile),
29
- `review-${runGit(['rev-parse', '--short', base])}..${runGit(['rev-parse', '--short', head])}.diff`
30
- );
31
- const content = [
32
- `# Review package: ${base}..${head}`,
33
- '',
34
- '## Commits',
35
- runGit(['log', '--oneline', `${base}..${head}`]),
36
- '',
37
- '## Files changed',
38
- runGit(['diff', '--stat', `${base}..${head}`]),
39
- '',
40
- '## Diff',
41
- runGit(['diff', '-U10', `${base}..${head}`])
42
- ].join('\n') + '\n';
43
- fs.writeFileSync(outputPath, content);
44
- console.log(`wrote ${outputPath}: ${commits} commit(s), ${Buffer.byteLength(content)} bytes`);
45
- }
46
-
47
- try {
48
- main();
49
- } catch (error) {
50
- console.error(error.message);
51
- process.exitCode = 2;
52
- }
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { runGit, workspaceForPlan } = require('./sdd-workspace-lib');
6
+
7
+ function gitStatus(args) {
8
+ const { spawnSync } = require('child_process');
9
+ return spawnSync('git', args, { encoding: 'utf8' });
10
+ }
11
+
12
+ function main() {
13
+ if (process.argv.length < 5 || process.argv.length > 6) {
14
+ throw new Error('usage: review-package PLAN_FILE BASE HEAD [OUTFILE]');
15
+ }
16
+ const [, , planFile, base, head, explicitOutput] = process.argv;
17
+ if (!fs.existsSync(path.resolve(planFile))) throw new Error(`no such plan file: ${planFile}`);
18
+ for (const ref of [base, head]) {
19
+ const result = gitStatus(['rev-parse', '--verify', '--quiet', ref]);
20
+ if (result.status !== 0) throw new Error(`bad ${ref === base ? 'BASE' : 'HEAD'}: ${ref}`);
21
+ }
22
+ const ancestor = gitStatus(['merge-base', '--is-ancestor', base, head]);
23
+ if (ancestor.status !== 0) throw new Error(`HEAD is not a descendant of BASE: ${base}..${head}`);
24
+ const commits = Number(runGit(['rev-list', '--count', `${base}..${head}`]));
25
+ if (commits <= 0) throw new Error(`empty commit range: ${base}..${head}`);
26
+
27
+ const outputPath = explicitOutput || path.join(
28
+ workspaceForPlan(planFile),
29
+ `review-${runGit(['rev-parse', '--short', base])}..${runGit(['rev-parse', '--short', head])}.diff`
30
+ );
31
+ const content = [
32
+ `# Review package: ${base}..${head}`,
33
+ '',
34
+ '## Commits',
35
+ runGit(['log', '--oneline', `${base}..${head}`]),
36
+ '',
37
+ '## Files changed',
38
+ runGit(['diff', '--stat', `${base}..${head}`]),
39
+ '',
40
+ '## Diff',
41
+ runGit(['diff', '-U10', `${base}..${head}`])
42
+ ].join('\n') + '\n';
43
+ fs.writeFileSync(outputPath, content);
44
+ console.log(`wrote ${outputPath}: ${commits} commit(s), ${Buffer.byteLength(content)} bytes`);
45
+ }
46
+
47
+ try {
48
+ main();
49
+ } catch (error) {
50
+ console.error(error.message);
51
+ process.exitCode = 2;
52
+ }
@@ -1,82 +1,82 @@
1
- #!/usr/bin/env bash
2
- # Resolve and ensure the working-tree directory SDD uses for one plan's
3
- # short-lived artifacts: task briefs, implementer reports, review packages,
4
- # and the progress ledger. Print the plan directory's absolute path.
5
- #
6
- # One directory per plan (.superpowers/sdd/<plan-basename>/) so a follow-up
7
- # plan in the same working tree can never read or overwrite another plan's
8
- # artifacts. A stale ledger misread as current progress makes controllers
9
- # skip whole task sequences — plan-scoping removes that failure structurally.
10
- #
11
- # Basename slugs collide when two plans share a filename (docs/alpha/plan.md
12
- # vs docs/beta/plan.md), so each workspace records its owning plan's path in
13
- # a plan-path marker (repo-relative in-repo, absolute outside). A workspace
14
- # owned by a different plan is skipped and the slug disambiguated with the
15
- # plan's parent-directory name, then a counter. A workspace with no marker
16
- # predates the marker scheme and is adopted for the current plan so in-flight
17
- # workspaces keep resolving — which means the first collision on such a
18
- # legacy workspace adopts instead of detecting; acceptable, marker-less
19
- # workspaces age out as plans finish.
20
- #
21
- # The workspace lives in the working tree (not under .git/) because Claude Code
22
- # treats .git/ as a protected path and denies agent writes there — which blocks
23
- # an implementer subagent from writing its report file. A self-ignoring
24
- # .gitignore at .superpowers/sdd/ keeps every plan's workspace out of
25
- # `git status` and out of accidental commits without modifying any tracked file.
26
- #
27
- # Single source of truth for the workspace location, so task-brief and
28
- # review-package cannot drift to different directories.
29
- #
30
- # Usage: sdd-workspace PLAN_FILE
31
- set -euo pipefail
32
-
33
- if [ $# -ne 1 ]; then
34
- echo "usage: sdd-workspace PLAN_FILE" >&2
35
- exit 2
36
- fi
37
-
38
- plan=$1
39
- [ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
40
-
41
- slug=$(basename "$plan" .md)
42
- [ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
43
- || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
44
-
45
- root=$(git rev-parse --show-toplevel)
46
- base="$root/.superpowers/sdd"
47
-
48
- # Normalize the plan path (physical directory, so relative/absolute/../
49
- # spellings of one plan compare equal) and express it as the marker value:
50
- # repo-relative when the plan lives under the repo root, absolute otherwise.
51
- plan_dir=$(CDPATH= cd -- "$(dirname "$plan")" && pwd -P)
52
- plan_abs="$plan_dir/$(basename "$plan")"
53
- case "$plan_abs" in
54
- "$root"/*) plan_id=${plan_abs#"$root"/} ;;
55
- *) plan_id=$plan_abs ;;
56
- esac
57
-
58
- # True when the workspace at $1 is (or becomes) this plan's: an existing
59
- # marker must name this plan; a missing marker means a new workspace or a
60
- # pre-marker legacy one, and either way the plan claims it by writing one.
61
- owns() {
62
- if [ -e "$1/plan-path" ]; then
63
- [ "$(cat "$1/plan-path")" = "$plan_id" ]
64
- else
65
- mkdir -p "$1"
66
- printf '%s\n' "$plan_id" > "$1/plan-path"
67
- fi
68
- }
69
-
70
- dir="$base/$slug"
71
- if ! owns "$dir"; then
72
- parent=$(basename "$plan_dir")
73
- dir="$base/$slug-$parent"
74
- if ! owns "$dir"; then
75
- n=2
76
- while ! owns "$base/$slug-$parent-$n"; do n=$((n + 1)); done
77
- dir="$base/$slug-$parent-$n"
78
- fi
79
- fi
80
-
81
- printf '*\n' > "$base/.gitignore"
82
- CDPATH= cd -- "$dir" && pwd
1
+ #!/usr/bin/env bash
2
+ # Resolve and ensure the working-tree directory SDD uses for one plan's
3
+ # short-lived artifacts: task briefs, implementer reports, review packages,
4
+ # and the progress ledger. Print the plan directory's absolute path.
5
+ #
6
+ # One directory per plan (.superpowers/sdd/<plan-basename>/) so a follow-up
7
+ # plan in the same working tree can never read or overwrite another plan's
8
+ # artifacts. A stale ledger misread as current progress makes controllers
9
+ # skip whole task sequences — plan-scoping removes that failure structurally.
10
+ #
11
+ # Basename slugs collide when two plans share a filename (docs/alpha/plan.md
12
+ # vs docs/beta/plan.md), so each workspace records its owning plan's path in
13
+ # a plan-path marker (repo-relative in-repo, absolute outside). A workspace
14
+ # owned by a different plan is skipped and the slug disambiguated with the
15
+ # plan's parent-directory name, then a counter. A workspace with no marker
16
+ # predates the marker scheme and is adopted for the current plan so in-flight
17
+ # workspaces keep resolving — which means the first collision on such a
18
+ # legacy workspace adopts instead of detecting; acceptable, marker-less
19
+ # workspaces age out as plans finish.
20
+ #
21
+ # The workspace lives in the working tree (not under .git/) because Claude Code
22
+ # treats .git/ as a protected path and denies agent writes there — which blocks
23
+ # an implementer subagent from writing its report file. A self-ignoring
24
+ # .gitignore at .superpowers/sdd/ keeps every plan's workspace out of
25
+ # `git status` and out of accidental commits without modifying any tracked file.
26
+ #
27
+ # Single source of truth for the workspace location, so task-brief and
28
+ # review-package cannot drift to different directories.
29
+ #
30
+ # Usage: sdd-workspace PLAN_FILE
31
+ set -euo pipefail
32
+
33
+ if [ $# -ne 1 ]; then
34
+ echo "usage: sdd-workspace PLAN_FILE" >&2
35
+ exit 2
36
+ fi
37
+
38
+ plan=$1
39
+ [ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
40
+
41
+ slug=$(basename "$plan" .md)
42
+ [ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
43
+ || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
44
+
45
+ root=$(git rev-parse --show-toplevel)
46
+ base="$root/.superpowers/sdd"
47
+
48
+ # Normalize the plan path (physical directory, so relative/absolute/../
49
+ # spellings of one plan compare equal) and express it as the marker value:
50
+ # repo-relative when the plan lives under the repo root, absolute otherwise.
51
+ plan_dir=$(CDPATH= cd -- "$(dirname "$plan")" && pwd -P)
52
+ plan_abs="$plan_dir/$(basename "$plan")"
53
+ case "$plan_abs" in
54
+ "$root"/*) plan_id=${plan_abs#"$root"/} ;;
55
+ *) plan_id=$plan_abs ;;
56
+ esac
57
+
58
+ # True when the workspace at $1 is (or becomes) this plan's: an existing
59
+ # marker must name this plan; a missing marker means a new workspace or a
60
+ # pre-marker legacy one, and either way the plan claims it by writing one.
61
+ owns() {
62
+ if [ -e "$1/plan-path" ]; then
63
+ [ "$(cat "$1/plan-path")" = "$plan_id" ]
64
+ else
65
+ mkdir -p "$1"
66
+ printf '%s\n' "$plan_id" > "$1/plan-path"
67
+ fi
68
+ }
69
+
70
+ dir="$base/$slug"
71
+ if ! owns "$dir"; then
72
+ parent=$(basename "$plan_dir")
73
+ dir="$base/$slug-$parent"
74
+ if ! owns "$dir"; then
75
+ n=2
76
+ while ! owns "$base/$slug-$parent-$n"; do n=$((n + 1)); done
77
+ dir="$base/$slug-$parent-$n"
78
+ fi
79
+ fi
80
+
81
+ printf '*\n' > "$base/.gitignore"
82
+ CDPATH= cd -- "$dir" && pwd
@@ -1,62 +1,62 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { spawnSync } = require('child_process');
4
-
5
- function runGit(args, cwd = process.cwd()) {
6
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
7
- if (result.error || result.status !== 0) {
8
- const detail = (result.stderr || result.error?.message || '').trim();
9
- throw new Error(detail || `git ${args.join(' ')} failed`);
10
- }
11
- return result.stdout.trim();
12
- }
13
-
14
- function assertPlan(planFile) {
15
- const planPath = path.resolve(planFile);
16
- if (!fs.existsSync(planPath) || !fs.statSync(planPath).isFile()) {
17
- throw new Error(`no such plan file: ${planFile}`);
18
- }
19
- return planPath;
20
- }
21
-
22
- function workspaceForPlan(planFile) {
23
- const planPath = assertPlan(planFile);
24
- const root = runGit(['rev-parse', '--show-toplevel']);
25
- const base = path.join(root, '.superpowers', 'sdd');
26
- const slug = path.basename(planPath, path.extname(planPath));
27
- if (!slug || slug === '.' || slug === '..') {
28
- throw new Error(`cannot derive a workspace name from: ${planFile}`);
29
- }
30
-
31
- const relative = path.relative(root, planPath).replace(/\\/g, '/');
32
- const planId = relative && !relative.startsWith('../') && relative !== '..' && !path.isAbsolute(relative)
33
- ? relative
34
- : planPath.replace(/\\/g, '/');
35
-
36
- function owns(candidate) {
37
- const marker = path.join(candidate, 'plan-path');
38
- if (fs.existsSync(marker)) {
39
- return fs.readFileSync(marker, 'utf8').trim() === planId;
40
- }
41
- fs.mkdirSync(candidate, { recursive: true });
42
- fs.writeFileSync(marker, `${planId}\n`);
43
- return true;
44
- }
45
-
46
- let directory = path.join(base, slug);
47
- if (!owns(directory)) {
48
- const parent = path.basename(path.dirname(planPath));
49
- directory = path.join(base, `${slug}-${parent}`);
50
- if (!owns(directory)) {
51
- let n = 2;
52
- while (!owns(path.join(base, `${slug}-${parent}-${n}`))) n += 1;
53
- directory = path.join(base, `${slug}-${parent}-${n}`);
54
- }
55
- }
56
-
57
- fs.mkdirSync(base, { recursive: true });
58
- fs.writeFileSync(path.join(base, '.gitignore'), '*\n');
59
- return path.resolve(directory);
60
- }
61
-
62
- module.exports = { assertPlan, runGit, workspaceForPlan };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ function runGit(args, cwd = process.cwd()) {
6
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
7
+ if (result.error || result.status !== 0) {
8
+ const detail = (result.stderr || result.error?.message || '').trim();
9
+ throw new Error(detail || `git ${args.join(' ')} failed`);
10
+ }
11
+ return result.stdout.trim();
12
+ }
13
+
14
+ function assertPlan(planFile) {
15
+ const planPath = path.resolve(planFile);
16
+ if (!fs.existsSync(planPath) || !fs.statSync(planPath).isFile()) {
17
+ throw new Error(`no such plan file: ${planFile}`);
18
+ }
19
+ return planPath;
20
+ }
21
+
22
+ function workspaceForPlan(planFile) {
23
+ const planPath = assertPlan(planFile);
24
+ const root = runGit(['rev-parse', '--show-toplevel']);
25
+ const base = path.join(root, '.superpowers', 'sdd');
26
+ const slug = path.basename(planPath, path.extname(planPath));
27
+ if (!slug || slug === '.' || slug === '..') {
28
+ throw new Error(`cannot derive a workspace name from: ${planFile}`);
29
+ }
30
+
31
+ const relative = path.relative(root, planPath).replace(/\\/g, '/');
32
+ const planId = relative && !relative.startsWith('../') && relative !== '..' && !path.isAbsolute(relative)
33
+ ? relative
34
+ : planPath.replace(/\\/g, '/');
35
+
36
+ function owns(candidate) {
37
+ const marker = path.join(candidate, 'plan-path');
38
+ if (fs.existsSync(marker)) {
39
+ return fs.readFileSync(marker, 'utf8').trim() === planId;
40
+ }
41
+ fs.mkdirSync(candidate, { recursive: true });
42
+ fs.writeFileSync(marker, `${planId}\n`);
43
+ return true;
44
+ }
45
+
46
+ let directory = path.join(base, slug);
47
+ if (!owns(directory)) {
48
+ const parent = path.basename(path.dirname(planPath));
49
+ directory = path.join(base, `${slug}-${parent}`);
50
+ if (!owns(directory)) {
51
+ let n = 2;
52
+ while (!owns(path.join(base, `${slug}-${parent}-${n}`))) n += 1;
53
+ directory = path.join(base, `${slug}-${parent}-${n}`);
54
+ }
55
+ }
56
+
57
+ fs.mkdirSync(base, { recursive: true });
58
+ fs.writeFileSync(path.join(base, '.gitignore'), '*\n');
59
+ return path.resolve(directory);
60
+ }
61
+
62
+ module.exports = { assertPlan, runGit, workspaceForPlan };
@@ -1,15 +1,15 @@
1
- #!/usr/bin/env node
2
-
3
- const { workspaceForPlan } = require('./sdd-workspace-lib');
4
-
5
- function main() {
6
- if (process.argv.length !== 3) throw new Error('usage: sdd-workspace PLAN_FILE');
7
- console.log(workspaceForPlan(process.argv[2]));
8
- }
9
-
10
- try {
11
- main();
12
- } catch (error) {
13
- console.error(error.message);
14
- process.exitCode = 2;
15
- }
1
+ #!/usr/bin/env node
2
+
3
+ const { workspaceForPlan } = require('./sdd-workspace-lib');
4
+
5
+ function main() {
6
+ if (process.argv.length !== 3) throw new Error('usage: sdd-workspace PLAN_FILE');
7
+ console.log(workspaceForPlan(process.argv[2]));
8
+ }
9
+
10
+ try {
11
+ main();
12
+ } catch (error) {
13
+ console.error(error.message);
14
+ process.exitCode = 2;
15
+ }
@@ -1,43 +1,43 @@
1
- #!/usr/bin/env bash
2
- # Extract one task's full text from an implementation plan into a file the
3
- # implementer reads in one call, so the task text never has to be pasted
4
- # through the controller's context.
5
- #
6
- # Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]
7
- # Default OUTFILE: <repo-root>/.superpowers/sdd/<plan-basename>/task-<N>-brief.md
8
- # (per plan and per worktree; concurrent runs of the SAME plan in the same
9
- # working tree share it).
10
- set -euo pipefail
11
-
12
- if [ $# -lt 2 ] || [ $# -gt 3 ]; then
13
- echo "usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]" >&2
14
- exit 2
15
- fi
16
-
17
- plan=$1
18
- n=$2
19
- [ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
20
-
21
- if [ $# -eq 3 ]; then
22
- out=$3
23
- else
24
- # Invoke via bash rather than direct exec: some extractors (Python zipfile)
25
- # strip Unix exec bits when unpacking marketplace packages (#2040).
26
- dir=$("${BASH:-bash}" "$(cd "$(dirname "$0")" && pwd)/sdd-workspace" "$plan")
27
- out="$dir/task-${n}-brief.md"
28
- fi
29
-
30
- awk -v n="$n" '
31
- /^```/ { infence = !infence }
32
- !infence && /^#+[ \t]+Task[ \t]+[0-9]+/ {
33
- intask = ($0 ~ ("^#+[ \t]+Task[ \t]+" n "([^0-9]|$)"))
34
- }
35
- intask { print }
36
- ' "$plan" > "$out"
37
-
38
- if [ ! -s "$out" ]; then
39
- echo "task ${n} not found in ${plan} (no heading matching 'Task ${n}')" >&2
40
- exit 3
41
- fi
42
-
43
- echo "wrote ${out}: $(wc -l < "$out" | tr -d ' ') lines"
1
+ #!/usr/bin/env bash
2
+ # Extract one task's full text from an implementation plan into a file the
3
+ # implementer reads in one call, so the task text never has to be pasted
4
+ # through the controller's context.
5
+ #
6
+ # Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]
7
+ # Default OUTFILE: <repo-root>/.superpowers/sdd/<plan-basename>/task-<N>-brief.md
8
+ # (per plan and per worktree; concurrent runs of the SAME plan in the same
9
+ # working tree share it).
10
+ set -euo pipefail
11
+
12
+ if [ $# -lt 2 ] || [ $# -gt 3 ]; then
13
+ echo "usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]" >&2
14
+ exit 2
15
+ fi
16
+
17
+ plan=$1
18
+ n=$2
19
+ [ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
20
+
21
+ if [ $# -eq 3 ]; then
22
+ out=$3
23
+ else
24
+ # Invoke via bash rather than direct exec: some extractors (Python zipfile)
25
+ # strip Unix exec bits when unpacking marketplace packages (#2040).
26
+ dir=$("${BASH:-bash}" "$(cd "$(dirname "$0")" && pwd)/sdd-workspace" "$plan")
27
+ out="$dir/task-${n}-brief.md"
28
+ fi
29
+
30
+ awk -v n="$n" '
31
+ /^```/ { infence = !infence }
32
+ !infence && /^#+[ \t]+Task[ \t]+[0-9]+/ {
33
+ intask = ($0 ~ ("^#+[ \t]+Task[ \t]+" n "([^0-9]|$)"))
34
+ }
35
+ intask { print }
36
+ ' "$plan" > "$out"
37
+
38
+ if [ ! -s "$out" ]; then
39
+ echo "task ${n} not found in ${plan} (no heading matching 'Task ${n}')" >&2
40
+ exit 3
41
+ fi
42
+
43
+ echo "wrote ${out}: $(wc -l < "$out" | tr -d ' ') lines"
@@ -1,46 +1,46 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { workspaceForPlan } = require('./sdd-workspace-lib');
6
-
7
- function extractTask(planText, taskNumber) {
8
- const lines = planText.split(/\r?\n/);
9
- const selected = [];
10
- let inFence = false;
11
- let active = false;
12
- for (const line of lines) {
13
- if (line.startsWith('```')) {
14
- inFence = !inFence;
15
- } else if (!inFence && /^#+[ \t]+Task[ \t]+[0-9]+/.test(line)) {
16
- const match = line.match(/^#+[ \t]+Task[ \t]+([0-9]+)(?:[^0-9]|$)/);
17
- active = Boolean(match && Number(match[1]) === taskNumber);
18
- }
19
- if (active) selected.push(line);
20
- }
21
- return selected;
22
- }
23
-
24
- function main() {
25
- if (process.argv.length < 4 || process.argv.length > 5) {
26
- throw new Error('usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]');
27
- }
28
- const planFile = process.argv[2];
29
- const taskNumber = Number(process.argv[3]);
30
- if (!Number.isInteger(taskNumber) || taskNumber < 1) throw new Error('TASK_NUMBER must be a positive integer');
31
- const planPath = path.resolve(planFile);
32
- if (!fs.existsSync(planPath)) throw new Error(`no such plan file: ${planFile}`);
33
- const outputPath = process.argv[4] || path.join(workspaceForPlan(planFile), `task-${taskNumber}-brief.md`);
34
- const selected = extractTask(fs.readFileSync(planPath, 'utf8'), taskNumber);
35
- if (selected.length === 0) throw new Error(`task ${taskNumber} not found in ${planFile} (no heading matching 'Task ${taskNumber}')`);
36
- const content = `${selected.join('\n')}\n`;
37
- fs.writeFileSync(outputPath, content);
38
- console.log(`wrote ${outputPath}: ${content.split(/\r?\n/).length - 1} lines`);
39
- }
40
-
41
- try {
42
- main();
43
- } catch (error) {
44
- console.error(error.message);
45
- process.exitCode = 2;
46
- }
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { workspaceForPlan } = require('./sdd-workspace-lib');
6
+
7
+ function extractTask(planText, taskNumber) {
8
+ const lines = planText.split(/\r?\n/);
9
+ const selected = [];
10
+ let inFence = false;
11
+ let active = false;
12
+ for (const line of lines) {
13
+ if (line.startsWith('```')) {
14
+ inFence = !inFence;
15
+ } else if (!inFence && /^#+[ \t]+Task[ \t]+[0-9]+/.test(line)) {
16
+ const match = line.match(/^#+[ \t]+Task[ \t]+([0-9]+)(?:[^0-9]|$)/);
17
+ active = Boolean(match && Number(match[1]) === taskNumber);
18
+ }
19
+ if (active) selected.push(line);
20
+ }
21
+ return selected;
22
+ }
23
+
24
+ function main() {
25
+ if (process.argv.length < 4 || process.argv.length > 5) {
26
+ throw new Error('usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]');
27
+ }
28
+ const planFile = process.argv[2];
29
+ const taskNumber = Number(process.argv[3]);
30
+ if (!Number.isInteger(taskNumber) || taskNumber < 1) throw new Error('TASK_NUMBER must be a positive integer');
31
+ const planPath = path.resolve(planFile);
32
+ if (!fs.existsSync(planPath)) throw new Error(`no such plan file: ${planFile}`);
33
+ const outputPath = process.argv[4] || path.join(workspaceForPlan(planFile), `task-${taskNumber}-brief.md`);
34
+ const selected = extractTask(fs.readFileSync(planPath, 'utf8'), taskNumber);
35
+ if (selected.length === 0) throw new Error(`task ${taskNumber} not found in ${planFile} (no heading matching 'Task ${taskNumber}')`);
36
+ const content = `${selected.join('\n')}\n`;
37
+ fs.writeFileSync(outputPath, content);
38
+ console.log(`wrote ${outputPath}: ${content.split(/\r?\n/).length - 1} lines`);
39
+ }
40
+
41
+ try {
42
+ main();
43
+ } catch (error) {
44
+ console.error(error.message);
45
+ process.exitCode = 2;
46
+ }