@g3nomaly/g3n-skills 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +3 -2
  2. package/package.json +1 -1
  3. package/skills/coding/.claude-plugin/plugin.json +2 -2
  4. package/skills/coding/analyze/SKILL.md +32 -0
  5. package/skills/coding/analyze/agents/openai.yaml +5 -0
  6. package/skills/coding/analyze/references/api.md +27 -0
  7. package/skills/coding/analyze/references/commands.md +54 -0
  8. package/skills/coding/analyze/references/repository.md +13 -0
  9. package/skills/coding/document/SKILL.md +26 -9
  10. package/skills/coding/document/references/typescript.md +44 -3
  11. package/skills/task-manager/decisions/src/paths.ts +13 -1
  12. package/skills/task-manager/decisions/src/read.ts +20 -6
  13. package/skills/task-manager/decisions/src/write.ts +28 -5
  14. package/skills/task-manager/task-manager/hooks/cleanup.ts +6 -0
  15. package/skills/task-manager/task-manager/hooks/guard-tree.ts +31 -4
  16. package/skills/task-manager/task-manager/hooks/sync-tree.ts +1 -1
  17. package/skills/task-manager/task-manager/src/filetree/ensure.ts +15 -3
  18. package/skills/task-manager/task-manager/src/filetree/entries.ts +26 -5
  19. package/skills/task-manager/task-manager/src/filetree/frontmatter.ts +23 -7
  20. package/skills/task-manager/task-manager/src/filetree/generate.ts +31 -6
  21. package/skills/task-manager/task-manager/src/filetree/levels.ts +1 -0
  22. package/skills/task-manager/task-manager/src/filetree/locate.ts +45 -13
  23. package/skills/task-manager/task-manager/src/filetree/paths.ts +30 -6
  24. package/skills/task-manager/task-manager/src/filetree/read.ts +15 -7
  25. package/skills/task-manager/task-manager/src/filetree/remove.ts +7 -4
  26. package/skills/task-manager/task-manager/src/filetree/root.ts +4 -3
  27. package/skills/task-manager/task-manager/src/filetree/status.ts +29 -7
  28. package/skills/task-manager/task-manager/src/hooks/cli.ts +50 -12
  29. package/skills/task-manager/task-manager/src/schemas/config.ts +1 -0
  30. package/skills/task-manager/task-manager/src/schemas/decision.ts +8 -2
  31. package/skills/task-manager/task-manager/src/schemas/phase.ts +6 -0
  32. package/skills/task-manager/task-manager/src/schemas/plan.ts +6 -0
  33. package/skills/task-manager/task-manager/src/schemas/primitives.ts +3 -0
  34. package/skills/task-manager/task-manager/src/schemas/task.ts +7 -0
  35. package/skills/task-manager/task-manager/src/schemas/term.ts +8 -1
  36. package/skills/task-manager/task-manager/src/schemas/validation.ts +31 -7
  37. package/skills/task-manager/task-manager/src/vocabulary/file.ts +37 -4
  38. package/skills/task-manager/task-manager/src/vocabulary/match.ts +29 -7
  39. package/skills/workflow/run/hooks/advance-step.ts +14 -2
  40. package/skills/workflow/run/src/gates.ts +21 -8
  41. package/skills/workflow/run/src/phase.ts +17 -3
  42. package/skills/workflow/run/src/steps.ts +10 -4
@@ -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
- /** One rejected field, as `field` reads in the source file. */
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
- * Thrown by `parseFile` when a file on disk does not match its schema.
12
- * Carries the path that was read so a caller can report it without tracking it.
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
- /** Turns a zod path into the dotted field name a reader can find in the file. */
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 file path of the file the data came from, used in the error message
38
- * @throws FileValidationError naming the file and every field that failed
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
- * Every term of the tree at `root`, in the order they sit in the file. A tree
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
- /** Replaces the vocabulary file with `terms`, sorted by name so the file reads the same on every write. */
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
- /** Splits the file at its `## ` headings; the last `_Avoid_:` line of a block is its avoid list. */
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
- /** A term found by name, and whether the name given was the term itself or a word it replaces. */
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
- /** A term a text touches, with the exact words that touched it. */
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
- /** The term answering to `name`, by its own name first and by an avoided word second. */
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
- * Terms `text` uses, by name or by an avoided word, in the order of `terms`.
38
- * Attached to an entry by the task-manager hooks so the reader gets each
39
- * definition along with the entry.
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
- /** Comma-separated ids of the phase's tasks in any of `statuses`, for an error message. */
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
- /** State the phase moves to once `from` is finished, or a stated reason it is not. */
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
- /** A phase that cannot start yet, and the phases it is waiting for. */
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
- /** A plan's phases sorted by whether their gate is open. Every list is in queue order. */
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. A phase's gate opens once every phase in its
21
- * `depends_on` is completed. Used by the `ready-phases` hook, which the
22
- * orchestrator skill calls between phases.
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 every phase of one plan, in queue order
25
- * @throws Error when a phase depends on a name no phase of the plan has
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
- /** A phase with everything the step hooks report on: its fields, its tasks, its step. */
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
- /** Reads the phase named `name` with its tasks, in queue order, and works out its step. */
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
- /** Where a phase stands: on a step, finished, or stuck on blocked work. */
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
- * Step a phase is on, worked out from its own status and its tasks' statuses.
24
- * Nothing else records the step, so a task sent back from review returns the
25
- * phase to `execute` without anything being told.
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";