@fledge/workflow 0.4.0 → 0.6.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.
@@ -3679,81 +3679,98 @@ const tasksFrontmatter = object({ tasks: array(object({
3679
3679
  //#region ../cli/dist/brief.js
3680
3680
  const BRIEFS_DIRECTORY = path.join(".fledge", "briefs");
3681
3681
  /**
3682
- * Returns the absolute path to the briefs directory for the current project.
3682
+ * Creates a BriefContext from a project directory path or falls back to `cwd()`.
3683
3683
  *
3684
- * @returns The absolute path to `.fledge/briefs/` in the current working directory.
3684
+ * @param projectDirectory - Optional project root override.
3685
+ * @returns A BriefContext with the resolved project root.
3685
3686
  */
3686
- function getBriefsDirectory() {
3687
- return path.join(cwd(), BRIEFS_DIRECTORY);
3687
+ function createBriefContext(projectDirectory) {
3688
+ return { projectRoot: projectDirectory ? path.resolve(projectDirectory) : cwd() };
3689
+ }
3690
+ /**
3691
+ * Returns the absolute path to the briefs directory for the given context.
3692
+ *
3693
+ * @param context - The brief context.
3694
+ * @returns The absolute path to `.fledge/briefs/` in the project root.
3695
+ */
3696
+ function getBriefsDirectory(context) {
3697
+ return path.join(context.projectRoot, BRIEFS_DIRECTORY);
3688
3698
  }
3689
3699
  /**
3690
3700
  * Returns the absolute path to a specific brief's directory.
3691
3701
  *
3702
+ * @param context - The brief context.
3692
3703
  * @param name - The brief name (used as directory name).
3693
3704
  * @returns The absolute path to the brief directory.
3694
3705
  */
3695
- function getBriefDirectory(name) {
3696
- return path.join(getBriefsDirectory(), name);
3706
+ function getBriefDirectory(context, name) {
3707
+ return path.join(getBriefsDirectory(context), name);
3697
3708
  }
3698
3709
  /**
3699
3710
  * Returns the absolute path to a brief's `brief.md` file.
3700
3711
  *
3712
+ * @param context - The brief context.
3701
3713
  * @param name - The brief name.
3702
3714
  * @returns The absolute path to `brief.md`.
3703
3715
  */
3704
- function getBriefFile(name) {
3705
- return path.join(getBriefDirectory(name), "brief.md");
3716
+ function getBriefFile(context, name) {
3717
+ return path.join(getBriefDirectory(context, name), "brief.md");
3706
3718
  }
3707
3719
  /**
3708
3720
  * Returns the absolute path to a brief's `tasks.md` file.
3709
3721
  *
3722
+ * @param context - The brief context.
3710
3723
  * @param name - The brief name.
3711
3724
  * @returns The absolute path to `tasks.md`.
3712
3725
  */
3713
- function getTasksFile(name) {
3714
- return path.join(getBriefDirectory(name), "tasks.md");
3726
+ function getTasksFile(context, name) {
3727
+ return path.join(getBriefDirectory(context, name), "tasks.md");
3715
3728
  }
3716
3729
  /**
3717
3730
  * Checks whether a brief directory exists.
3718
3731
  *
3732
+ * @param context - The brief context.
3719
3733
  * @param name - The brief name.
3720
3734
  * @returns `true` if the brief directory exists.
3721
3735
  */
3722
- function briefExists(name) {
3723
- return fs.existsSync(getBriefDirectory(name));
3736
+ function briefExists(context, name) {
3737
+ return fs.existsSync(getBriefDirectory(context, name));
3724
3738
  }
3725
3739
  /**
3726
3740
  * Reads and validates the frontmatter of a brief's `brief.md` file.
3727
3741
  *
3742
+ * @param context - The brief context.
3728
3743
  * @param name - The brief name.
3729
3744
  * @returns The validated brief frontmatter.
3730
3745
  * @throws If the file does not exist or frontmatter fails validation.
3731
3746
  */
3732
- function readBriefFrontmatter(name) {
3733
- const content = fs.readFileSync(getBriefFile(name), "utf8");
3747
+ function readBriefFrontmatter(context, name) {
3748
+ const content = fs.readFileSync(getBriefFile(context, name), "utf8");
3734
3749
  return briefFrontmatter.parse(parseFrontmatter(content));
3735
3750
  }
3736
3751
  /**
3737
3752
  * Reads and validates the frontmatter of a brief's `tasks.md` file.
3738
3753
  *
3754
+ * @param context - The brief context.
3739
3755
  * @param name - The brief name.
3740
3756
  * @returns The validated tasks frontmatter.
3741
3757
  * @throws If the file does not exist or frontmatter fails validation.
3742
3758
  */
3743
- function readTasksFrontmatter(name) {
3744
- const content = fs.readFileSync(getTasksFile(name), "utf8");
3759
+ function readTasksFrontmatter(context, name) {
3760
+ const content = fs.readFileSync(getTasksFile(context, name), "utf8");
3745
3761
  return tasksFrontmatter.parse(parseFrontmatter(content));
3746
3762
  }
3747
3763
  /**
3748
3764
  * Updates the frontmatter of a brief's `brief.md` file while preserving its body content.
3749
3765
  *
3766
+ * @param context - The brief context.
3750
3767
  * @param name - The brief name.
3751
3768
  * @param data - The new frontmatter data to write.
3752
3769
  */
3753
- function updateBriefFrontmatter(name, data) {
3754
- const briefFile = getBriefFile(name);
3755
- const content = fs.readFileSync(briefFile, "utf8");
3756
- fs.writeFileSync(briefFile, writeFrontmatter(data, content));
3770
+ function updateBriefFrontmatter(context, name, data) {
3771
+ const file = getBriefFile(context, name);
3772
+ const content = fs.readFileSync(file, "utf8");
3773
+ fs.writeFileSync(file, writeFrontmatter(data, content));
3757
3774
  }
3758
3775
  /**
3759
3776
  * Validates that a state transition is allowed and throws a descriptive error if not.
@@ -3780,37 +3797,52 @@ function formatDate() {
3780
3797
  /**
3781
3798
  * Lists all brief directories in the project.
3782
3799
  *
3800
+ * @param context - The brief context.
3783
3801
  * @returns An array of brief names (directory names).
3784
3802
  */
3785
- function listBriefs() {
3786
- const directory = getBriefsDirectory();
3803
+ function listBriefs(context) {
3804
+ const directory = getBriefsDirectory(context);
3787
3805
  if (!fs.existsSync(directory)) return [];
3788
3806
  return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3789
3807
  }
3790
3808
  //#endregion
3809
+ //#region ../cli/dist/commands/brief/shared.js
3810
+ /**
3811
+ * Shared argument definition for the `--project-dir` flag used by all brief subcommands.
3812
+ */
3813
+ const projectDirectory = {
3814
+ required: false,
3815
+ alias: "project-dir",
3816
+ description: "Project root directory. Overrides cwd() for locating .fledge/briefs/."
3817
+ };
3818
+ //#endregion
3791
3819
  //#region ../cli/dist/commands/brief/complete.js
3792
3820
  var complete_default = defineCommand({
3793
3821
  meta: {
3794
3822
  name: "complete",
3795
3823
  description: "Transition a brief from active to completed"
3796
3824
  },
3797
- args: { name: {
3798
- type: "positional",
3799
- required: true,
3800
- description: "The name of the brief to complete"
3801
- } },
3825
+ args: {
3826
+ name: {
3827
+ type: "positional",
3828
+ required: true,
3829
+ description: "The name of the brief to complete"
3830
+ },
3831
+ projectDirectory
3832
+ },
3802
3833
  run(context) {
3834
+ const ctx = createBriefContext(context.args.projectDirectory);
3803
3835
  const { name } = context.args;
3804
- if (!briefExists(name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(name)}"`);
3805
- const brief = readBriefFrontmatter(name);
3836
+ if (!briefExists(ctx, name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(ctx, name)}"`);
3837
+ const brief = readBriefFrontmatter(ctx, name);
3806
3838
  validateTransition(brief.status, "completed");
3807
3839
  if (!brief.summary) throw new Error("Brief must have a summary before completing. Add a \"summary\" field to the brief.md frontmatter.");
3808
- const incomplete = readTasksFrontmatter(name).tasks.filter((task) => !task.done);
3840
+ const incomplete = readTasksFrontmatter(ctx, name).tasks.filter((task) => !task.done);
3809
3841
  if (incomplete.length > 0) {
3810
3842
  const names = incomplete.map((task) => ` - ${task.name}`).join("\n");
3811
3843
  throw new Error(`Cannot complete brief with ${incomplete.length} incomplete task(s):\n${names}`);
3812
3844
  }
3813
- updateBriefFrontmatter(name, {
3845
+ updateBriefFrontmatter(ctx, name, {
3814
3846
  ...brief,
3815
3847
  status: "completed",
3816
3848
  updated: formatDate()
@@ -3825,24 +3857,28 @@ var create_default = defineCommand({
3825
3857
  name: "create",
3826
3858
  description: "Create a new brief with stub files"
3827
3859
  },
3828
- args: { name: {
3829
- type: "positional",
3830
- required: true,
3831
- description: "The name of the brief to create (used as directory name)"
3832
- } },
3860
+ args: {
3861
+ name: {
3862
+ type: "positional",
3863
+ required: true,
3864
+ description: "The name of the brief to create (used as directory name)"
3865
+ },
3866
+ projectDirectory
3867
+ },
3833
3868
  run(context) {
3869
+ const ctx = createBriefContext(context.args.projectDirectory);
3834
3870
  const { name } = context.args;
3835
- if (briefExists(name)) throw new Error(`Brief "${name}" already exists at "${getBriefDirectory(name)}"`);
3836
- const directory = getBriefDirectory(name);
3871
+ if (briefExists(ctx, name)) throw new Error(`Brief "${name}" already exists at "${getBriefDirectory(ctx, name)}"`);
3872
+ const directory = getBriefDirectory(ctx, name);
3837
3873
  fs.mkdirSync(directory, { recursive: true });
3838
3874
  const date = formatDate();
3839
- fs.writeFileSync(getBriefFile(name), writeFrontmatter({
3875
+ fs.writeFileSync(getBriefFile(ctx, name), writeFrontmatter({
3840
3876
  name,
3841
3877
  status: "draft",
3842
3878
  created: date,
3843
3879
  updated: date
3844
3880
  }));
3845
- fs.writeFileSync(getTasksFile(name), writeFrontmatter({ tasks: [] }));
3881
+ fs.writeFileSync(getTasksFile(ctx, name), writeFrontmatter({ tasks: [] }));
3846
3882
  stdout.write(`Created brief "${name}" at "${directory}"\n`);
3847
3883
  }
3848
3884
  });
@@ -3853,20 +3889,24 @@ var list_default = defineCommand({
3853
3889
  name: "list",
3854
3890
  description: "List all briefs with their status and progress"
3855
3891
  },
3856
- args: { status: {
3857
- type: "string",
3858
- required: false,
3859
- description: "Filter by brief status (draft, active, completed)"
3860
- } },
3892
+ args: {
3893
+ status: {
3894
+ type: "string",
3895
+ required: false,
3896
+ description: "Filter by brief status (draft, active, completed)"
3897
+ },
3898
+ projectDirectory
3899
+ },
3861
3900
  run(context) {
3862
- const names = listBriefs();
3901
+ const ctx = createBriefContext(context.args.projectDirectory);
3902
+ const names = listBriefs(ctx);
3863
3903
  if (names.length === 0) {
3864
3904
  stdout.write("No briefs found\n");
3865
3905
  return;
3866
3906
  }
3867
3907
  let briefs = names.map((name) => {
3868
- const brief = readBriefFrontmatter(name);
3869
- const { tasks } = readTasksFrontmatter(name);
3908
+ const brief = readBriefFrontmatter(ctx, name);
3909
+ const { tasks } = readTasksFrontmatter(ctx, name);
3870
3910
  const done = tasks.filter((task) => task.done).length;
3871
3911
  return {
3872
3912
  ...brief,
@@ -3916,18 +3956,22 @@ var start_default = defineCommand({
3916
3956
  name: "start",
3917
3957
  description: "Transition a brief from draft to active"
3918
3958
  },
3919
- args: { name: {
3920
- type: "positional",
3921
- required: true,
3922
- description: "The name of the brief to start"
3923
- } },
3959
+ args: {
3960
+ name: {
3961
+ type: "positional",
3962
+ required: true,
3963
+ description: "The name of the brief to start"
3964
+ },
3965
+ projectDirectory
3966
+ },
3924
3967
  run(context) {
3968
+ const ctx = createBriefContext(context.args.projectDirectory);
3925
3969
  const { name } = context.args;
3926
- if (!briefExists(name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(name)}"`);
3927
- const brief = readBriefFrontmatter(name);
3970
+ if (!briefExists(ctx, name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(ctx, name)}"`);
3971
+ const brief = readBriefFrontmatter(ctx, name);
3928
3972
  validateTransition(brief.status, "active");
3929
- if (readTasksFrontmatter(name).tasks.length === 0) throw new Error("Brief must have at least one task before starting");
3930
- updateBriefFrontmatter(name, {
3973
+ if (readTasksFrontmatter(ctx, name).tasks.length === 0) throw new Error("Brief must have at least one task before starting");
3974
+ updateBriefFrontmatter(ctx, name, {
3931
3975
  ...brief,
3932
3976
  status: "active",
3933
3977
  updated: formatDate()
@@ -3980,24 +4024,28 @@ runMain(defineCommand({
3980
4024
  name: "status",
3981
4025
  description: "Show the status and task progress of a brief"
3982
4026
  },
3983
- args: { name: {
3984
- type: "positional",
3985
- required: true,
3986
- description: "The name of the brief to show status for"
3987
- } },
4027
+ args: {
4028
+ name: {
4029
+ type: "positional",
4030
+ required: true,
4031
+ description: "The name of the brief to show status for"
4032
+ },
4033
+ projectDirectory
4034
+ },
3988
4035
  run(context) {
4036
+ const ctx = createBriefContext(context.args.projectDirectory);
3989
4037
  const { name } = context.args;
3990
- if (!briefExists(name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(name)}"`);
3991
- const brief = readBriefFrontmatter(name);
3992
- const { tasks } = readTasksFrontmatter(name);
4038
+ if (!briefExists(ctx, name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(ctx, name)}"`);
4039
+ const brief = readBriefFrontmatter(ctx, name);
4040
+ const { tasks } = readTasksFrontmatter(ctx, name);
3993
4041
  const done = tasks.filter((task) => task.done).length;
3994
4042
  const lines = [];
3995
4043
  lines.push(`${name} [${brief.status}] ${done}/${tasks.length} tasks done`);
3996
4044
  lines.push("");
3997
4045
  const groups = groupTasks(tasks);
3998
- for (const [group, groupTasks] of groups) {
4046
+ for (const [group, groupedTasks] of groups) {
3999
4047
  if (group) lines.push(group);
4000
- for (const task of groupTasks) lines.push(formatTask(task));
4048
+ for (const task of groupedTasks) lines.push(formatTask(task));
4001
4049
  }
4002
4050
  stdout.write(`${lines.join("\n")}\n`);
4003
4051
  }
@@ -4007,19 +4055,23 @@ runMain(defineCommand({
4007
4055
  name: "validate",
4008
4056
  description: "Validate brief and tasks files against their schemas"
4009
4057
  },
4010
- args: { name: {
4011
- type: "positional",
4012
- required: true,
4013
- description: "The name of the brief to validate"
4014
- } },
4058
+ args: {
4059
+ name: {
4060
+ type: "positional",
4061
+ required: true,
4062
+ description: "The name of the brief to validate"
4063
+ },
4064
+ projectDirectory
4065
+ },
4015
4066
  run(context) {
4067
+ const ctx = createBriefContext(context.args.projectDirectory);
4016
4068
  const { name } = context.args;
4017
- if (!briefExists(name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(name)}"`);
4069
+ if (!briefExists(ctx, name)) throw new Error(`Brief "${name}" does not exist at "${getBriefDirectory(ctx, name)}"`);
4018
4070
  const errors = [];
4019
- const briefContent = fs.readFileSync(getBriefFile(name), "utf8");
4071
+ const briefContent = fs.readFileSync(getBriefFile(ctx, name), "utf8");
4020
4072
  const briefResult = briefFrontmatter.safeParse(parseFrontmatter(briefContent));
4021
4073
  if (!briefResult.success) for (const issue of briefResult.error.issues) errors.push(`brief.md: ${issue.path.join(".")}: ${issue.message}`);
4022
- const tasksContent = fs.readFileSync(getTasksFile(name), "utf8");
4074
+ const tasksContent = fs.readFileSync(getTasksFile(ctx, name), "utf8");
4023
4075
  const tasksResult = tasksFrontmatter.safeParse(parseFrontmatter(tasksContent));
4024
4076
  if (!tasksResult.success) for (const issue of tasksResult.error.issues) errors.push(`tasks.md: ${issue.path.join(".")}: ${issue.message}`);
4025
4077
  if (errors.length > 0) throw new Error(`Validation failed for "${name}":\n${errors.map((error) => ` - ${error}`).join("\n")}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fledge/workflow",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.6.0",
5
5
  "author": "René Schapka",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -15,7 +15,7 @@
15
15
  "node": ">=24"
16
16
  },
17
17
  "dependencies": {
18
- "@fledge/cli": "^0.7.0"
18
+ "@fledge/cli": "^0.9.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@antfu/eslint-config": "7.7.3",
@@ -5,18 +5,19 @@ description: >-
5
5
  Invoked directly via /fledge-brief, not auto-triggered.
6
6
  metadata:
7
7
  type: workflow
8
+ allowed-tools: Bash(scripts/brief.js *)
8
9
  ---
9
10
 
10
11
  ## Available scripts
11
12
 
12
- Self-contained scripts bundled with this skill. Run from the skill directory with `node`:
13
+ Self-contained executable scripts bundled with this skill. All brief commands require `--project-dir` to point to the project root:
13
14
 
14
- - **`scripts/brief.js create <name>`** -- Create a new brief with stub files
15
- - **`scripts/brief.js start <name>`** -- Transition a brief from draft to active
16
- - **`scripts/brief.js complete <name>`** -- Transition a brief from active to completed
17
- - **`scripts/brief.js status <name>`** -- Show status and task progress
18
- - **`scripts/brief.js list [--status <status>]`** -- List all briefs with progress and summary
19
- - **`scripts/brief.js validate <name>`** -- Validate brief files against schemas
15
+ - **`scripts/brief.js create <name> --project-dir <path>`** -- Create a new brief with stub files
16
+ - **`scripts/brief.js start <name> --project-dir <path>`** -- Transition a brief from draft to active
17
+ - **`scripts/brief.js complete <name> --project-dir <path>`** -- Transition a brief from active to completed
18
+ - **`scripts/brief.js status <name> --project-dir <path>`** -- Show status and task progress
19
+ - **`scripts/brief.js list [--status <status>] --project-dir <path>`** -- List all briefs with progress and summary
20
+ - **`scripts/brief.js validate <name> --project-dir <path>`** -- Validate brief files against schemas
20
21
  - **`scripts/brief.js schema`** -- Output JSON Schema for brief and tasks frontmatter
21
22
 
22
23
  ## Step 0: Determine intent
@@ -27,7 +28,7 @@ Ask what the user wants to do, or infer from context. Present these options:
27
28
  2. **Continue a brief** -- pick up an existing brief. Proceed to Step 4.
28
29
  3. **Complete a brief** -- wrap up a finished feature. Proceed to Step 5.
29
30
 
30
- If unclear, run `node scripts/brief.js list` to show current briefs and ask.
31
+ If unclear, run `scripts/brief.js list --project-dir <path>` to show current briefs and ask.
31
32
 
32
33
  ---
33
34
 
@@ -35,7 +36,7 @@ If unclear, run `node scripts/brief.js list` to show current briefs and ask.
35
36
 
36
37
  Before writing anything, build an understanding of what exists.
37
38
 
38
- 1. Run `node scripts/brief.js list --status completed` to read summaries of completed features. Note anything relevant to the new feature.
39
+ 1. Run `scripts/brief.js list --status completed --project-dir <path>` to read summaries of completed features. Note anything relevant to the new feature.
39
40
  2. Ask the user what they want to build. Keep it conversational, not a form. Aim to understand:
40
41
  - What is the user-facing change?
41
42
  - Why does it matter?
@@ -50,7 +51,7 @@ Proceed to Step 2.
50
51
 
51
52
  ## Step 2: Draft the brief
52
53
 
53
- Run `node scripts/brief.js create <name>` to create the brief directory.
54
+ Run `scripts/brief.js create <name> --project-dir <path>` to create the brief directory.
54
55
 
55
56
  Write the brief content into `brief.md`. The frontmatter is managed by the scripts. The markdown body should capture:
56
57
 
@@ -86,7 +87,7 @@ tasks:
86
87
 
87
88
  Order tasks by dependency: tasks that others depend on come first within their group.
88
89
 
89
- After writing tasks, run `node scripts/brief.js validate <name>` to confirm the brief is valid, then run `node scripts/brief.js start <name>` to transition to active.
90
+ After writing tasks, run `scripts/brief.js validate <name> --project-dir <path>` to confirm the brief is valid, then run `scripts/brief.js start <name> --project-dir <path>` to transition to active.
90
91
 
91
92
  Present the complete brief and task list to the user for review before starting.
92
93
 
@@ -94,22 +95,22 @@ Present the complete brief and task list to the user for review before starting.
94
95
 
95
96
  ## Step 4: Continue a brief
96
97
 
97
- Run `node scripts/brief.js list` to show all briefs. If the user does not specify which brief, ask them to pick one.
98
+ Run `scripts/brief.js list --project-dir <path>` to show all briefs. If the user does not specify which brief, ask them to pick one.
98
99
 
99
- Run `node scripts/brief.js status <name>` to show progress. Read the brief and tasks files to understand the full context.
100
+ Run `scripts/brief.js status <name> --project-dir <path>` to show progress. Read the brief and tasks files to understand the full context.
100
101
 
101
102
  From here, the user may want to:
102
103
  - **Discuss a task** -- talk through approach before implementing
103
104
  - **Update tasks** -- mark tasks as done, add new tasks, reorder
104
105
  - **Revise the brief** -- update scope or design decisions based on what was learned during implementation
105
106
 
106
- When updating task status, modify the `tasks.md` frontmatter directly, then run `node scripts/brief.js status <name>` to confirm the update.
107
+ When updating task status, modify the `tasks.md` frontmatter directly, then run `scripts/brief.js status <name> --project-dir <path>` to confirm the update.
107
108
 
108
109
  ---
109
110
 
110
111
  ## Step 5: Complete a brief
111
112
 
112
- Run `node scripts/brief.js status <name>` to verify all tasks are done.
113
+ Run `scripts/brief.js status <name> --project-dir <path>` to verify all tasks are done.
113
114
 
114
115
  If there are incomplete tasks, ask the user whether to:
115
116
  1. Mark remaining tasks as done (if they were completed outside this conversation)
@@ -120,4 +121,4 @@ Write a summary into the `brief.md` frontmatter `summary` field. The summary sho
120
121
  - What was built
121
122
  - Key decisions or patterns established that future features should know about
122
123
 
123
- Run `node scripts/brief.js complete <name>` to transition to completed. The script validates that all tasks are done and the summary is present.
124
+ Run `scripts/brief.js complete <name> --project-dir <path>` to transition to completed. The script validates that all tasks are done and the summary is present.