@g3nomaly/g3n-skills 0.1.0 → 0.1.2
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/.claude-plugin/marketplace.json +3 -2
- package/README.md +96 -0
- package/package.json +1 -1
- package/skills/coding/.claude-plugin/plugin.json +2 -2
- package/skills/coding/analyze/SKILL.md +32 -0
- package/skills/coding/analyze/agents/openai.yaml +5 -0
- package/skills/coding/analyze/references/api.md +27 -0
- package/skills/coding/analyze/references/commands.md +54 -0
- package/skills/coding/analyze/references/repository.md +13 -0
- package/skills/coding/document/SKILL.md +26 -9
- package/skills/coding/document/references/typescript.md +44 -3
- package/skills/task-manager/decisions/src/paths.ts +13 -1
- package/skills/task-manager/decisions/src/read.ts +20 -6
- package/skills/task-manager/decisions/src/write.ts +28 -5
- package/skills/task-manager/task-manager/hooks/cleanup.ts +6 -0
- package/skills/task-manager/task-manager/hooks/guard-tree.ts +31 -4
- package/skills/task-manager/task-manager/hooks/sync-tree.ts +1 -1
- package/skills/task-manager/task-manager/src/filetree/ensure.ts +15 -3
- package/skills/task-manager/task-manager/src/filetree/entries.ts +26 -5
- package/skills/task-manager/task-manager/src/filetree/frontmatter.ts +23 -7
- package/skills/task-manager/task-manager/src/filetree/generate.ts +31 -6
- package/skills/task-manager/task-manager/src/filetree/levels.ts +1 -0
- package/skills/task-manager/task-manager/src/filetree/locate.ts +45 -13
- package/skills/task-manager/task-manager/src/filetree/paths.ts +30 -6
- package/skills/task-manager/task-manager/src/filetree/read.ts +15 -7
- package/skills/task-manager/task-manager/src/filetree/remove.ts +7 -4
- package/skills/task-manager/task-manager/src/filetree/root.ts +4 -3
- package/skills/task-manager/task-manager/src/filetree/status.ts +29 -7
- package/skills/task-manager/task-manager/src/hooks/cli.ts +50 -12
- package/skills/task-manager/task-manager/src/schemas/config.ts +1 -0
- package/skills/task-manager/task-manager/src/schemas/decision.ts +8 -2
- package/skills/task-manager/task-manager/src/schemas/phase.ts +6 -0
- package/skills/task-manager/task-manager/src/schemas/plan.ts +6 -0
- package/skills/task-manager/task-manager/src/schemas/primitives.ts +3 -0
- package/skills/task-manager/task-manager/src/schemas/task.ts +7 -0
- package/skills/task-manager/task-manager/src/schemas/term.ts +8 -1
- package/skills/task-manager/task-manager/src/schemas/validation.ts +31 -7
- package/skills/task-manager/task-manager/src/vocabulary/file.ts +37 -4
- package/skills/task-manager/task-manager/src/vocabulary/match.ts +29 -7
- package/skills/workflow/run/hooks/advance-step.ts +14 -2
- package/skills/workflow/run/src/gates.ts +21 -8
- package/skills/workflow/run/src/phase.ts +17 -3
- package/skills/workflow/run/src/steps.ts +10 -4
|
@@ -6,6 +6,7 @@ export const DECISIONS_DIR = "decisions";
|
|
|
6
6
|
|
|
7
7
|
export const DECISION_STATUSES = ["proposed", "accepted", "deprecated", "superseded"] as const;
|
|
8
8
|
export const DecisionStatusSchema = z.enum(DECISION_STATUSES);
|
|
9
|
+
/** Status of a decision file, one of `DECISION_STATUSES`. */
|
|
9
10
|
export type DecisionStatus = z.infer<typeof DecisionStatusSchema>;
|
|
10
11
|
|
|
11
12
|
/** A decision id: four digits, a hyphen, then a slug — e.g. `0001-use-jwt`. Doubles as the file name. */
|
|
@@ -18,16 +19,21 @@ export const DecisionSchema = z.strictObject({
|
|
|
18
19
|
id: DecisionIdSchema,
|
|
19
20
|
title: z.string().min(1),
|
|
20
21
|
status: DecisionStatusSchema,
|
|
21
|
-
/** Day the decision was recorded, as `YYYY-MM-DD` */
|
|
22
22
|
date: z.iso.date(),
|
|
23
|
-
/** Id of the earlier decision this one replaces */
|
|
24
23
|
supersedes: DecisionIdSchema.optional(),
|
|
25
24
|
});
|
|
26
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Frontmatter of one decision file in a plan's `decisions/` folder, as `DecisionSchema` parses it.
|
|
28
|
+
*
|
|
29
|
+
* @property date - Day the decision was recorded, as `YYYY-MM-DD`.
|
|
30
|
+
* @property supersedes - Id of the earlier decision this one replaces.
|
|
31
|
+
*/
|
|
27
32
|
export type Decision = z.infer<typeof DecisionSchema>;
|
|
28
33
|
|
|
29
34
|
export const DecisionIndexSchema = z.strictObject({
|
|
30
35
|
decisions: z.array(DecisionSchema).default([]),
|
|
31
36
|
});
|
|
32
37
|
|
|
38
|
+
/** Index of the decisions in a plan's `decisions/` folder, as `DecisionIndexSchema` parses it. */
|
|
33
39
|
export type DecisionIndex = z.infer<typeof DecisionIndexSchema>;
|
|
@@ -12,10 +12,16 @@ export const PhaseSchema = z.strictObject({
|
|
|
12
12
|
status: StatusSchema,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Frontmatter of one phase file, as `PhaseSchema` parses it.
|
|
17
|
+
*
|
|
18
|
+
* @property depends_on - Names of the phases of the same plan that must be completed first.
|
|
19
|
+
*/
|
|
15
20
|
export type Phase = z.infer<typeof PhaseSchema>;
|
|
16
21
|
|
|
17
22
|
export const PhaseIndexSchema = z.strictObject({
|
|
18
23
|
phases: z.array(PhaseSchema).default([]),
|
|
19
24
|
});
|
|
20
25
|
|
|
26
|
+
/** Every phase of one plan, as `PhaseIndexSchema` parses it. */
|
|
21
27
|
export type PhaseIndex = z.infer<typeof PhaseIndexSchema>;
|
|
@@ -11,10 +11,16 @@ export const PlanSchema = z.strictObject({
|
|
|
11
11
|
status: StatusSchema,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Frontmatter of one plan file, as `PlanSchema` parses it.
|
|
16
|
+
*
|
|
17
|
+
* @property depends_on - Names of the plans that must be completed first.
|
|
18
|
+
*/
|
|
14
19
|
export type Plan = z.infer<typeof PlanSchema>;
|
|
15
20
|
|
|
16
21
|
export const PlanIndexSchema = z.strictObject({
|
|
17
22
|
plans: z.array(PlanSchema).default([]),
|
|
18
23
|
});
|
|
19
24
|
|
|
25
|
+
/** Every plan under the tree root, as `PlanIndexSchema` parses it. */
|
|
20
26
|
export type PlanIndex = z.infer<typeof PlanIndexSchema>;
|
|
@@ -5,10 +5,12 @@ import { DECISIONS_DIR } from "./decision";
|
|
|
5
5
|
/** Status folder names, in the order a task moves through them. */
|
|
6
6
|
export const STATUSES = ["open", "in-progress", "in-review", "completed", "blocked"] as const;
|
|
7
7
|
export const StatusSchema = z.enum(STATUSES);
|
|
8
|
+
/** Status of a plan, phase or task, one of `STATUSES`. */
|
|
8
9
|
export type TaskStatus = z.infer<typeof StatusSchema>;
|
|
9
10
|
|
|
10
11
|
export const PRIORITIES = ["critical", "high", "medium", "low"] as const;
|
|
11
12
|
export const PrioritySchema = z.enum(PRIORITIES);
|
|
13
|
+
/** Priority of a plan, phase or task, one of `PRIORITIES`. */
|
|
12
14
|
export type TaskPriority = z.infer<typeof PrioritySchema>;
|
|
13
15
|
|
|
14
16
|
/** Branch-name prefixes for a task worked in its own git worktree. */
|
|
@@ -23,6 +25,7 @@ export const WORKTREE_TYPES = [
|
|
|
23
25
|
"experiment",
|
|
24
26
|
] as const;
|
|
25
27
|
export const WorktreeTypeSchema = z.enum(WORKTREE_TYPES);
|
|
28
|
+
/** Branch-name prefix of a task's worktree, one of `WORKTREE_TYPES`. */
|
|
26
29
|
export type WorktreeType = z.infer<typeof WorktreeTypeSchema>;
|
|
27
30
|
|
|
28
31
|
/** A lowercase, hyphen-separated name usable verbatim as one directory segment. */
|
|
@@ -15,10 +15,17 @@ export const TaskSchema = z.strictObject({
|
|
|
15
15
|
status: StatusSchema,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Frontmatter of one task file, as `TaskSchema` parses it.
|
|
20
|
+
*
|
|
21
|
+
* @property worktree - Always `true`: every task is worked in its own git worktree.
|
|
22
|
+
* @property complexity - Effort from 1 to 5, when set.
|
|
23
|
+
*/
|
|
18
24
|
export type Task = z.infer<typeof TaskSchema>;
|
|
19
25
|
|
|
20
26
|
export const TaskIndexSchema = z.strictObject({
|
|
21
27
|
tasks: z.array(TaskSchema).default([]),
|
|
22
28
|
});
|
|
23
29
|
|
|
30
|
+
/** Every task of one phase, as `TaskIndexSchema` parses it. */
|
|
24
31
|
export type TaskIndex = z.infer<typeof TaskIndexSchema>;
|
|
@@ -16,16 +16,23 @@ export const TermSchema = z.strictObject({
|
|
|
16
16
|
(text) => !text.split("\n").some((line) => line.startsWith("#") || line.startsWith("_Avoid_:")),
|
|
17
17
|
"must not contain a line starting with # or _Avoid_:",
|
|
18
18
|
),
|
|
19
|
-
/** Words that mean the same thing and must not be used in its place */
|
|
20
19
|
avoid: z
|
|
21
20
|
.array(TermNameSchema.refine((word) => !word.includes(","), "must not contain a comma"))
|
|
22
21
|
.default([]),
|
|
23
22
|
});
|
|
24
23
|
|
|
24
|
+
/**
|
|
25
|
+
* One entry of `vocabulary.md`, as `TermSchema` parses it.
|
|
26
|
+
*
|
|
27
|
+
* @property term - The word to use.
|
|
28
|
+
* @property definition - What the term means, as written under its heading.
|
|
29
|
+
* @property avoid - Words that mean the same thing and must not be used in its place.
|
|
30
|
+
*/
|
|
25
31
|
export type Term = z.infer<typeof TermSchema>;
|
|
26
32
|
|
|
27
33
|
export const VocabularySchema = z.strictObject({
|
|
28
34
|
terms: z.array(TermSchema).default([]),
|
|
29
35
|
});
|
|
30
36
|
|
|
37
|
+
/** Every term of `vocabulary.md`, as `VocabularySchema` parses it. */
|
|
31
38
|
export type Vocabulary = z.infer<typeof VocabularySchema>;
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/schemas/validation.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* One rejected field of a file, as `parseFile` reports it.
|
|
6
|
+
*
|
|
7
|
+
* @property field - Dotted path of the field as it reads in the file, e.g. `phases.0.status`, or `(whole file)`.
|
|
8
|
+
* @property message - Why the field was rejected, e.g. `is required`.
|
|
9
|
+
*/
|
|
5
10
|
export interface FileValidationIssue {
|
|
6
11
|
field: string;
|
|
7
12
|
message: string;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* @brief Error for a file on disk that does not match its schema.
|
|
17
|
+
*
|
|
18
|
+
* Holds the path of the file and one issue per rejected field, and its message
|
|
19
|
+
* lists them all. Thrown by `parseFile` and by the readers in `filetree/`,
|
|
20
|
+
* `decisions/` and `vocabulary/` that check a file's layout by hand. Callers
|
|
21
|
+
* tell it apart from other errors with `instanceof FileValidationError`.
|
|
13
22
|
*/
|
|
14
23
|
export class FileValidationError extends Error {
|
|
15
24
|
readonly file: string;
|
|
16
25
|
readonly issues: FileValidationIssue[];
|
|
17
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @brief Builds the message from the file path and every issue.
|
|
29
|
+
*
|
|
30
|
+
* @param file - Path of the file that failed, e.g. `/home/me/app/docs/task-manager/auth/auth.md`.
|
|
31
|
+
* @param issues - Rejected fields, e.g. `{ field: "phases.0.status", message: "is required" }`.
|
|
32
|
+
*/
|
|
18
33
|
constructor(file: string, issues: FileValidationIssue[]) {
|
|
19
34
|
const detail = issues.map((i) => ` ${i.field}: ${i.message}`).join("\n");
|
|
20
35
|
super(`${file} does not match its schema:\n${detail}`);
|
|
@@ -24,18 +39,27 @@ export class FileValidationError extends Error {
|
|
|
24
39
|
}
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* @brief Turns a zod issue path into the dotted field name a reader can find in the file.
|
|
44
|
+
*
|
|
45
|
+
* @param path - Segments of the issue path, e.g. `["phases", 0, "status"]`.
|
|
46
|
+
* @returns Dotted name, or `(whole file)` for an empty path.
|
|
47
|
+
*/
|
|
28
48
|
function fieldName(path: ReadonlyArray<PropertyKey>): string {
|
|
29
49
|
if (path.length === 0) return "(whole file)";
|
|
30
50
|
return path.map((segment) => String(segment)).join(".");
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
/**
|
|
34
|
-
* Checks data read from a file against its schema.
|
|
54
|
+
* @brief Checks data read from a file against its schema.
|
|
55
|
+
*
|
|
35
56
|
* Every file read from disk goes through here before it is handed on.
|
|
36
57
|
*
|
|
37
|
-
* @param
|
|
38
|
-
* @
|
|
58
|
+
* @param schema - Zod schema of the file's layout.
|
|
59
|
+
* @param file - Path of the file the data came from, used in the error message.
|
|
60
|
+
* @param data - Raw data read from the file, before validation.
|
|
61
|
+
* @returns The data as the schema parses it.
|
|
62
|
+
* @throws FileValidationError naming the file and every field that failed.
|
|
39
63
|
*/
|
|
40
64
|
export function parseFile<S extends z.ZodType>(
|
|
41
65
|
schema: S,
|
|
@@ -10,14 +10,21 @@ const TITLE = "# Vocabulary";
|
|
|
10
10
|
const TERM_HEADING = "## ";
|
|
11
11
|
const AVOID_PREFIX = "_Avoid_:";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @brief Builds the path of the vocabulary file in a tree.
|
|
15
|
+
*
|
|
16
|
+
* @param root - Absolute tree root folder.
|
|
17
|
+
* @returns Path of `vocabulary.md` inside `root`.
|
|
18
|
+
*/
|
|
13
19
|
export function vocabularyPathFor(root: string): string {
|
|
14
20
|
return join(root, VOCABULARY_FILE);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
|
-
*
|
|
19
|
-
* with no vocabulary file yet has no terms.
|
|
24
|
+
* @brief Reads every term of the tree at `root`, in the order they sit in the file.
|
|
20
25
|
*
|
|
26
|
+
* @param root - Absolute tree root folder.
|
|
27
|
+
* @returns Terms in file order, or an empty list when there is no vocabulary file yet.
|
|
21
28
|
* @throws FileValidationError when the file strays from the layout
|
|
22
29
|
* `writeVocabulary` produces, or names one term twice
|
|
23
30
|
*/
|
|
@@ -31,7 +38,12 @@ export async function readVocabulary(root: string): Promise<Term[]> {
|
|
|
31
38
|
return terms;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* @brief Replaces the vocabulary file with `terms`, sorted by name so the file reads the same on every write.
|
|
43
|
+
*
|
|
44
|
+
* @param root - Absolute tree root folder.
|
|
45
|
+
* @param terms - Terms to write; validated and checked for duplicate names first.
|
|
46
|
+
*/
|
|
35
47
|
export async function writeVocabulary(root: string, terms: readonly Term[]): Promise<void> {
|
|
36
48
|
const file = vocabularyPathFor(root);
|
|
37
49
|
const validated = parseFile(VocabularySchema, file, { terms }).terms;
|
|
@@ -44,6 +56,11 @@ export async function writeVocabulary(root: string, terms: readonly Term[]): Pro
|
|
|
44
56
|
await Bun.write(file, `${[TITLE, ...sorted.map(renderTerm)].join("\n\n")}\n`);
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @brief Renders one term as its `## ` block in the vocabulary file.
|
|
61
|
+
*
|
|
62
|
+
* @param term - Term to render.
|
|
63
|
+
*/
|
|
47
64
|
function renderTerm(term: Term): string {
|
|
48
65
|
const lines = [`${TERM_HEADING}${term.term}`, "", term.definition.trim()];
|
|
49
66
|
if (term.avoid.length > 0) lines.push("", `${AVOID_PREFIX} ${term.avoid.join(", ")}`);
|
|
@@ -51,7 +68,16 @@ function renderTerm(term: Term): string {
|
|
|
51
68
|
return lines.join("\n");
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* @brief Splits the file text at its `## ` headings into raw term entries.
|
|
73
|
+
*
|
|
74
|
+
* The last `_Avoid_:` line of a block is its avoid list.
|
|
75
|
+
*
|
|
76
|
+
* @param file - Path of the file, used in the error message.
|
|
77
|
+
* @param text - Full text of the vocabulary file.
|
|
78
|
+
* @returns One raw entry per heading, ready for `parseFile`.
|
|
79
|
+
* @throws FileValidationError when text sits above the first heading.
|
|
80
|
+
*/
|
|
55
81
|
function parseTerms(file: string, text: string): unknown[] {
|
|
56
82
|
const terms: { term: string; lines: string[] }[] = [];
|
|
57
83
|
|
|
@@ -85,6 +111,13 @@ function parseTerms(file: string, text: string): unknown[] {
|
|
|
85
111
|
});
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @brief Throws when two terms share a name, ignoring case.
|
|
116
|
+
*
|
|
117
|
+
* @param file - Path of the file, used in the error message.
|
|
118
|
+
* @param terms - Terms to check.
|
|
119
|
+
* @throws FileValidationError naming the term that is defined twice.
|
|
120
|
+
*/
|
|
88
121
|
function rejectDuplicates(file: string, terms: readonly Term[]): void {
|
|
89
122
|
const seen = new Set<string>();
|
|
90
123
|
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/task-manager/task-manager/src/vocabulary/match.ts
|
|
2
2
|
import type { Term } from "../schemas/term";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Term found by `findTerm`, with how the name matched it.
|
|
6
|
+
*
|
|
7
|
+
* @property term - The matched term.
|
|
8
|
+
* @property matched - `"term"` when the name is the term itself, `"avoid"` when it is one of the term's avoided words.
|
|
9
|
+
*/
|
|
5
10
|
export interface TermMatch {
|
|
6
11
|
term: Term;
|
|
7
12
|
matched: "term" | "avoid";
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Term a text uses, with the avoided words the text used for it.
|
|
17
|
+
*
|
|
18
|
+
* @property term - Name of the term.
|
|
19
|
+
* @property definition - Definition of the term, as the vocabulary file holds it.
|
|
20
|
+
* @property avoided - Avoided words the text uses in place of `term`.
|
|
21
|
+
*/
|
|
11
22
|
export interface TermMention {
|
|
12
23
|
term: string;
|
|
13
24
|
definition: string;
|
|
14
|
-
/** Avoided words the text uses in place of `term` */
|
|
15
25
|
avoided: string[];
|
|
16
26
|
}
|
|
17
27
|
|
|
28
|
+
/** Tells whether two words are equal, ignoring case. */
|
|
18
29
|
function sameWord(a: string, b: string): boolean {
|
|
19
30
|
return a.toLowerCase() === b.toLowerCase();
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* @brief Finds the term that answers to `name`, by its own name first and by an avoided word second.
|
|
35
|
+
*
|
|
36
|
+
* @param terms - Vocabulary to search.
|
|
37
|
+
* @param name - Word to look up, compared ignoring case.
|
|
38
|
+
* @returns The match, or `null` when no term has the name.
|
|
39
|
+
*/
|
|
23
40
|
export function findTerm(terms: readonly Term[], name: string): TermMatch | null {
|
|
24
41
|
const own = terms.find((term) => sameWord(term.term, name));
|
|
25
42
|
if (own !== undefined) return { term: own, matched: "term" };
|
|
@@ -28,15 +45,20 @@ export function findTerm(terms: readonly Term[], name: string): TermMatch | null
|
|
|
28
45
|
return replaced === undefined ? null : { term: replaced, matched: "avoid" };
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
/** Tells whether `word` occurs in `text` as a whole word, ignoring case. */
|
|
31
49
|
function mentions(text: string, word: string): boolean {
|
|
32
50
|
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
51
|
return new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu").test(text);
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
55
|
+
* @brief Finds the terms `text` uses, by name or by an avoided word, in the order of `terms`.
|
|
56
|
+
*
|
|
57
|
+
* Used by the `get-entry` hook, which attaches each mention to its entry.
|
|
58
|
+
*
|
|
59
|
+
* @param text - Text to search, such as an entry's title, summary and body.
|
|
60
|
+
* @param terms - Vocabulary to search.
|
|
61
|
+
* @returns One mention per term the text uses.
|
|
40
62
|
*/
|
|
41
63
|
export function termsMentioned(text: string, terms: readonly Term[]): TermMention[] {
|
|
42
64
|
const found: TermMention[] = [];
|
|
@@ -4,12 +4,24 @@ import { oneOf, printJson, requireFlag, resolveRoot, runHook } from "@task-manag
|
|
|
4
4
|
import { readPhaseRun, type PhaseRun } from "../src/phase";
|
|
5
5
|
import { PHASE_STATUS_AT, STEPS, stepFor, type Step, type StepState } from "../src/steps";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* @brief Lists the ids of the phase's tasks in any of `statuses`, comma-separated, for an error message.
|
|
9
|
+
*
|
|
10
|
+
* @param run - Phase whose tasks are checked.
|
|
11
|
+
* @param statuses - Task statuses to match.
|
|
12
|
+
*/
|
|
8
13
|
function idsWith(run: PhaseRun, ...statuses: string[]): string {
|
|
9
14
|
return run.tasks.filter((task) => statuses.includes(task.status)).map((task) => task.id).join(", ");
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* @brief Works out the state a phase moves to once step `from` is finished.
|
|
19
|
+
*
|
|
20
|
+
* @param run - Phase to advance.
|
|
21
|
+
* @param from - Step being finished.
|
|
22
|
+
* @returns State the phase moves to.
|
|
23
|
+
* @throws Error naming the reason when the step cannot be finished yet.
|
|
24
|
+
*/
|
|
13
25
|
function stateAfter(run: PhaseRun, from: Step): StepState {
|
|
14
26
|
const name = run.location.name;
|
|
15
27
|
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// packages/g3n-skills/skills/workflow/run/src/gates.ts
|
|
2
2
|
import type { Phase } from "@task-manager/schemas";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A phase whose gate is closed, and the dependencies it is waiting for.
|
|
6
|
+
*
|
|
7
|
+
* @property name - Name of the waiting phase.
|
|
8
|
+
* @property waiting_on - Dependencies that are not completed yet.
|
|
9
|
+
*/
|
|
5
10
|
export interface WaitingPhase {
|
|
6
11
|
name: string;
|
|
7
12
|
waiting_on: string[];
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A plan's phases sorted by whether their gate is open. Every list is in queue order.
|
|
17
|
+
*
|
|
18
|
+
* @property ready - Phases neither finished nor blocked, whose every dependency is completed.
|
|
19
|
+
* @property waiting - Phases with at least one dependency not completed yet.
|
|
20
|
+
* @property blocked - Names of the blocked phases.
|
|
21
|
+
* @property completed - Names of the completed phases.
|
|
22
|
+
*/
|
|
11
23
|
export interface PlanGates {
|
|
12
|
-
/** Not finished, not blocked, and every phase it depends on is completed */
|
|
13
24
|
ready: Phase[];
|
|
14
25
|
waiting: WaitingPhase[];
|
|
15
26
|
blocked: string[];
|
|
@@ -17,12 +28,14 @@ export interface PlanGates {
|
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
* Sorts a plan's phases by gate.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
31
|
+
* @brief Sorts a plan's phases by gate.
|
|
32
|
+
*
|
|
33
|
+
* A phase's gate opens once every phase in its `depends_on` is completed.
|
|
34
|
+
* Used by the `ready-phases` hook.
|
|
23
35
|
*
|
|
24
|
-
* @param phases
|
|
25
|
-
* @
|
|
36
|
+
* @param phases - Every phase of one plan, in queue order.
|
|
37
|
+
* @returns The phases sorted by gate.
|
|
38
|
+
* @throws Error when a phase depends on a name no phase of the plan has.
|
|
26
39
|
*/
|
|
27
40
|
export function gatesFor(phases: readonly Phase[]): PlanGates {
|
|
28
41
|
const statusOf = new Map(phases.map((phase) => [phase.name, phase.status]));
|
|
@@ -3,10 +3,18 @@ import { childDirFor, listEntryNames, locateEntryOfType, readEntry, type EntryLo
|
|
|
3
3
|
import type { Phase, Task } from "@task-manager/schemas";
|
|
4
4
|
import { stepFor, type StepState } from "./steps";
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* A phase with everything the step hooks report on, as `readPhaseRun` builds it.
|
|
8
|
+
*
|
|
9
|
+
* @property location - Where the phase sits in the tree.
|
|
10
|
+
* @property plan - Name of the plan holding the phase.
|
|
11
|
+
* @property meta - Frontmatter of the phase.
|
|
12
|
+
* @property body - Markdown body of the phase file.
|
|
13
|
+
* @property tasks - Tasks of the phase, in queue order.
|
|
14
|
+
* @property step - Step the phase is on, worked out from its status and its tasks.
|
|
15
|
+
*/
|
|
7
16
|
export interface PhaseRun {
|
|
8
17
|
location: EntryLocation;
|
|
9
|
-
/** Name of the plan holding the phase */
|
|
10
18
|
plan: string;
|
|
11
19
|
meta: Phase;
|
|
12
20
|
body: string;
|
|
@@ -14,7 +22,13 @@ export interface PhaseRun {
|
|
|
14
22
|
step: StepState;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* @brief Reads the phase named `name` with its tasks, in queue order, and works out its step.
|
|
27
|
+
*
|
|
28
|
+
* @param root - Absolute tree root folder.
|
|
29
|
+
* @param name - Name of the phase, e.g. `01-schema`.
|
|
30
|
+
* @returns The phase with its tasks and step.
|
|
31
|
+
*/
|
|
18
32
|
export async function readPhaseRun(root: string, name: string): Promise<PhaseRun> {
|
|
19
33
|
const location = await locateEntryOfType(root, name, "phase");
|
|
20
34
|
const { meta, body } = await readEntry(location.dir, "plan", location.name);
|
|
@@ -4,9 +4,10 @@ import type { TaskStatus } from "@task-manager/schemas/primitives";
|
|
|
4
4
|
/** The steps a phase is worked through, in order. Each has a workflow skill of the same name. */
|
|
5
5
|
export const STEPS = ["plan", "execute", "review", "sync"] as const;
|
|
6
6
|
|
|
7
|
+
/** One step a phase is worked through: a member of `STEPS`. */
|
|
7
8
|
export type Step = (typeof STEPS)[number];
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
+
/** State a phase stands in: one of the `STEPS`, `done`, or `blocked`. */
|
|
10
11
|
export type StepState = Step | "done" | "blocked";
|
|
11
12
|
|
|
12
13
|
/** Status a phase carries while it stands at each state. */
|
|
@@ -20,9 +21,14 @@ export const PHASE_STATUS_AT: Record<StepState, TaskStatus> = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* @brief Works out the step a phase is on, from its own status and its tasks' statuses.
|
|
25
|
+
*
|
|
26
|
+
* The step is not stored. A task sent back from review therefore returns the
|
|
27
|
+
* phase to `execute` on its own.
|
|
28
|
+
*
|
|
29
|
+
* @param phaseStatus - Status in the phase's frontmatter.
|
|
30
|
+
* @param taskStatuses - Statuses of the phase's tasks.
|
|
31
|
+
* @returns The step the phase is on.
|
|
26
32
|
*/
|
|
27
33
|
export function stepFor(phaseStatus: TaskStatus, taskStatuses: readonly TaskStatus[]): StepState {
|
|
28
34
|
if (phaseStatus === "completed") return "done";
|