@emiliosp/pi-maestro 0.5.2 → 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.
Files changed (65) hide show
  1. package/README.md +38 -41
  2. package/agents/builder.md +16 -18
  3. package/agents/verifier.md +24 -21
  4. package/docs/configuration.md +16 -24
  5. package/docs/subagent-integration.md +18 -40
  6. package/docs/workflow.md +106 -169
  7. package/package.json +1 -2
  8. package/src/MaestroPaths.ts +111 -47
  9. package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +2 -15
  10. package/src/artifacts/builder-handoff/readBuilderHandoff.ts +0 -3
  11. package/src/artifacts/builder-handoff/schema.ts +1 -1
  12. package/src/artifacts/builder-handoff/writeBuilderHandoff.ts +3 -5
  13. package/src/artifacts/escalation/createEscalation.ts +7 -7
  14. package/src/artifacts/escalation/getNextEscalationId.ts +0 -3
  15. package/src/artifacts/escalation/readEscalation.ts +1 -11
  16. package/src/artifacts/escalation/readEscalationHistory.ts +0 -3
  17. package/src/artifacts/escalation/resolveEscalation.ts +2 -12
  18. package/src/artifacts/escalation/schema.ts +0 -1
  19. package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +2 -9
  20. package/src/artifacts/verifier-handoff/readVerifierHandoff.ts +0 -3
  21. package/src/artifacts/verifier-handoff/schema.ts +20 -6
  22. package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +7 -7
  23. package/src/config/loadConfiguration.ts +18 -18
  24. package/src/maestro/checks/assertEnvironment.ts +13 -17
  25. package/src/maestro/instructions/getMaestroInstructions.ts +33 -24
  26. package/src/specs/create.ts +0 -2
  27. package/src/tools/child/open-escalation.ts +3 -4
  28. package/src/tools/child/record-builder-handoff.ts +4 -4
  29. package/src/tools/child/record-verifier-handoff.ts +13 -26
  30. package/src/tools/child/utils/resolveWorkflowContext.ts +3 -3
  31. package/src/tools/main/mark-spec-ready.ts +2 -2
  32. package/src/tools/main/resolve-escalation.ts +2 -4
  33. package/src/tools/main/resolve-findings.ts +17 -15
  34. package/src/tools/main/run-builder.ts +10 -38
  35. package/src/tools/main/run-verifier.ts +6 -33
  36. package/src/tools/utils/resolveToolRunContext.ts +8 -8
  37. package/src/utils/path-strictly-within.ts +4 -4
  38. package/src/utils/write-json.ts +24 -0
  39. package/src/workflow/builder/completeBuilderPass.ts +6 -15
  40. package/src/workflow/builder/prepareBuilderRun.ts +3 -34
  41. package/src/workflow/escalation/openBuilderEscalation.ts +2 -10
  42. package/src/workflow/escalation/resolveBuilderEscalation.ts +9 -14
  43. package/src/workflow/findings/resolveFindings.ts +25 -135
  44. package/src/workflow/spec/markSpecReady.ts +0 -1
  45. package/src/workflow/state/readWorkflowState.ts +2 -2
  46. package/src/workflow/state/schema.ts +0 -1
  47. package/src/workflow/state/writeWorkflowState.ts +7 -63
  48. package/src/workflow/transitions.ts +2 -2
  49. package/src/workflow/verifier/completeVerifierPass.ts +8 -14
  50. package/src/workflow/verifier/prepareVerifierRun.ts +4 -41
  51. package/src/artifacts/verifier-handoff/rejectVerifierFinding.ts +0 -54
  52. package/src/git/command.ts +0 -172
  53. package/src/git/commits/createCommit.ts +0 -76
  54. package/src/git/commits/createWorkflowCheckpointCommit.ts +0 -24
  55. package/src/git/commits/findCommitByMessage.ts +0 -39
  56. package/src/git/commits/getStagedPaths.ts +0 -23
  57. package/src/git/history/getParentCommit.ts +0 -25
  58. package/src/git/repository/assertRepositoryTrusted.ts +0 -16
  59. package/src/git/repository/findRepositoryRoot.ts +0 -19
  60. package/src/git/repository/getCurrentBranch.ts +0 -18
  61. package/src/git/repository/getHeadCommit.ts +0 -18
  62. package/src/git/repository/getRepositoryStatus.ts +0 -74
  63. package/src/git/utils/hasGitExitCode.ts +0 -19
  64. package/src/utils/write-atomically.ts +0 -41
  65. package/src/utils/write-json-atomically.ts +0 -28
@@ -1,172 +0,0 @@
1
- /**
2
- * Objective: Run Git commands with consistent errors and timeouts.
3
- * Used: By Maestro Git modules for every Git operation.
4
- */
5
-
6
- import { execFile } from 'node:child_process';
7
- import { promisify } from 'node:util';
8
-
9
- const execFileAsync = promisify(execFile);
10
-
11
- export const DEFAULT_GIT_TIMEOUT_MS = 10_000;
12
-
13
- export type GitCommandResult = {
14
- arguments: readonly string[];
15
- cwd: string;
16
- stdout: string;
17
- stderr: string;
18
- exitCode: number;
19
- };
20
-
21
- export const GIT_COMMAND_ERROR_CODES = {
22
- COMMAND_FAILED: 'command-failed',
23
- EXECUTION_FAILED: 'execution-failed',
24
- NOT_FOUND: 'not-found',
25
- TIMEOUT: 'timeout',
26
- } as const;
27
-
28
- export type GitCommandErrorCode =
29
- (typeof GIT_COMMAND_ERROR_CODES)[keyof typeof GIT_COMMAND_ERROR_CODES];
30
-
31
- type GitCommandErrorInput = {
32
- code: GitCommandErrorCode;
33
- message: string;
34
- arguments: readonly string[];
35
- cwd: string;
36
- stdout: string;
37
- stderr: string;
38
- exitCode?: number | null;
39
- cause: unknown;
40
- };
41
-
42
- export class GitCommandError extends Error {
43
- readonly code: GitCommandErrorCode;
44
- readonly arguments: readonly string[];
45
- readonly cwd: string;
46
- readonly stdout: string;
47
- readonly stderr: string;
48
- readonly exitCode: number | null;
49
-
50
- constructor({
51
- code,
52
- message,
53
- arguments: gitArguments,
54
- cwd,
55
- stdout,
56
- stderr,
57
- exitCode = null,
58
- cause,
59
- }: GitCommandErrorInput) {
60
- super(message, { cause });
61
- this.name = 'GitCommandError';
62
- this.code = code;
63
- this.arguments = gitArguments;
64
- this.cwd = cwd;
65
- this.stdout = stdout;
66
- this.stderr = stderr;
67
- this.exitCode = exitCode;
68
- }
69
- }
70
-
71
- type GitExecutionError = {
72
- message: string;
73
- code?: string | number;
74
- killed?: boolean;
75
- stdout?: string;
76
- stderr?: string;
77
- };
78
-
79
- const isGitExecutionError = (value: unknown): value is GitExecutionError =>
80
- value instanceof Error;
81
-
82
- const isNumericExitCode = (value: unknown): value is number =>
83
- typeof value === 'number';
84
-
85
- type RunGitCommandInput = {
86
- arguments: readonly string[];
87
- cwd?: string;
88
- timeoutMs?: number;
89
- environment?: NodeJS.ProcessEnv;
90
- };
91
-
92
- // git <arguments>
93
- export const runGitCommand = async ({
94
- arguments: gitArguments,
95
- cwd = process.cwd(),
96
- timeoutMs = DEFAULT_GIT_TIMEOUT_MS,
97
- environment,
98
- }: RunGitCommandInput): Promise<GitCommandResult> => {
99
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
100
- throw new RangeError('Git command timeout must be a positive integer.');
101
- }
102
-
103
- try {
104
- const { stdout, stderr } = await execFileAsync('git', [...gitArguments], {
105
- cwd,
106
- encoding: 'utf8',
107
- env: environment,
108
- killSignal: 'SIGKILL',
109
- timeout: timeoutMs,
110
- windowsHide: true,
111
- });
112
-
113
- return {
114
- arguments: gitArguments,
115
- cwd,
116
- stdout,
117
- stderr,
118
- exitCode: 0,
119
- };
120
- } catch (error) {
121
- const executionError: GitExecutionError = isGitExecutionError(error)
122
- ? error
123
- : new Error('Git command failed with an unknown error.', {
124
- cause: error,
125
- });
126
-
127
- const stdout = executionError.stdout ?? '';
128
- const stderr = executionError.stderr ?? '';
129
-
130
- if (executionError.killed) {
131
- throw new GitCommandError({
132
- code: GIT_COMMAND_ERROR_CODES.TIMEOUT,
133
- message: `Git command timed out after ${timeoutMs} ms.`,
134
- arguments: gitArguments,
135
- cwd,
136
- stdout,
137
- stderr,
138
- cause: error,
139
- });
140
- }
141
-
142
- if (executionError.code === 'ENOENT') {
143
- throw new GitCommandError({
144
- code: GIT_COMMAND_ERROR_CODES.NOT_FOUND,
145
- message: 'Git executable was not found.',
146
- arguments: gitArguments,
147
- cwd,
148
- stdout,
149
- stderr,
150
- cause: error,
151
- });
152
- }
153
-
154
- const exitCode = isNumericExitCode(executionError.code)
155
- ? executionError.code
156
- : null;
157
-
158
- throw new GitCommandError({
159
- code:
160
- exitCode === null
161
- ? GIT_COMMAND_ERROR_CODES.EXECUTION_FAILED
162
- : GIT_COMMAND_ERROR_CODES.COMMAND_FAILED,
163
- message: `Git command failed: ${executionError.message}`,
164
- arguments: gitArguments,
165
- cwd,
166
- stdout,
167
- stderr,
168
- exitCode,
169
- cause: error,
170
- });
171
- }
172
- };
@@ -1,76 +0,0 @@
1
- /**
2
- * Objective: Create a checkpoint commit containing exactly the expected paths.
3
- * Used: When Maestro records a workflow checkpoint.
4
- */
5
-
6
- import { isAbsolute, relative } from 'node:path';
7
- import { runGitCommand } from '#git/command.ts';
8
- import { getStagedPaths } from '#git/commits/getStagedPaths.ts';
9
- import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
10
-
11
- export const WORKFLOW_CHECKPOINT_COMMIT_MESSAGE = 'maestro workflow checkpoint';
12
-
13
- type HasSamePathsInput = {
14
- actual: readonly string[];
15
- expected: readonly string[];
16
- };
17
-
18
- const hasSamePaths = ({ actual, expected }: HasSamePathsInput): boolean => {
19
- if (actual.length !== expected.length) {
20
- return false;
21
- }
22
-
23
- return actual.every((path) => expected.includes(path));
24
- };
25
-
26
- // git add <path>...
27
- // git diff --cached --name-only -z
28
- // git commit --message <message>
29
- // git rev-parse --verify HEAD^{commit}
30
- type CreateCommitInput = {
31
- repositoryRoot: string;
32
- expectedPaths: readonly string[];
33
- message?: string;
34
- };
35
-
36
- export const createCommit = async ({
37
- repositoryRoot,
38
- expectedPaths,
39
- message = WORKFLOW_CHECKPOINT_COMMIT_MESSAGE,
40
- }: CreateCommitInput): Promise<string> => {
41
- const expectedGitPaths = expectedPaths.map((path) => {
42
- if (
43
- !isAbsolute(path) ||
44
- !isPathStrictlyWithin({ parent: repositoryRoot, candidate: path })
45
- ) {
46
- throw new Error(
47
- `Checkpoint path must be inside the repository: ${path}.`,
48
- );
49
- }
50
-
51
- return relative(repositoryRoot, path);
52
- });
53
-
54
- await runGitCommand({
55
- arguments: ['add', '--', ...expectedPaths],
56
- cwd: repositoryRoot,
57
- });
58
-
59
- const stagedPaths = await getStagedPaths(repositoryRoot);
60
-
61
- if (!hasSamePaths({ actual: stagedPaths, expected: expectedGitPaths })) {
62
- throw new Error('Checkpoint has staged paths outside the expected set.');
63
- }
64
-
65
- await runGitCommand({
66
- arguments: ['commit', '--message', message],
67
- cwd: repositoryRoot,
68
- });
69
-
70
- const result = await runGitCommand({
71
- arguments: ['rev-parse', '--verify', 'HEAD^{commit}'],
72
- cwd: repositoryRoot,
73
- });
74
-
75
- return result.stdout.trim();
76
- };
@@ -1,24 +0,0 @@
1
- /**
2
- * Objective: Create a workflow checkpoint commit containing exactly the expected paths.
3
- * Used: When Maestro records a workflow checkpoint.
4
- */
5
-
6
- import {
7
- createCommit,
8
- WORKFLOW_CHECKPOINT_COMMIT_MESSAGE,
9
- } from '#git/commits/createCommit.ts';
10
-
11
- type CreateWorkflowCheckpointCommitInput = {
12
- repositoryRoot: string;
13
- expectedPaths: readonly string[];
14
- };
15
-
16
- export const createWorkflowCheckpointCommit = async ({
17
- repositoryRoot,
18
- expectedPaths,
19
- }: CreateWorkflowCheckpointCommitInput): Promise<string> =>
20
- createCommit({
21
- repositoryRoot,
22
- expectedPaths,
23
- message: WORKFLOW_CHECKPOINT_COMMIT_MESSAGE,
24
- });
@@ -1,39 +0,0 @@
1
- /**
2
- * Objective: Find a commit by its exact subject.
3
- * Used: When Maestro finds a checkpoint by its subject.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // Finds a commit whose subject exactly matches the given message.
9
- // git log --all --format=%H%x00%s --fixed-strings --grep=<message>
10
- type FindCommitByMessageInput = {
11
- repositoryRoot: string;
12
- message: string;
13
- };
14
-
15
- export const findCommitByMessage = async ({
16
- repositoryRoot,
17
- message,
18
- }: FindCommitByMessageInput): Promise<string | undefined> => {
19
- const result = await runGitCommand({
20
- arguments: [
21
- 'log',
22
- '--all',
23
- '--format=%H%x00%s',
24
- '--fixed-strings',
25
- `--grep=${message}`,
26
- ],
27
- cwd: repositoryRoot,
28
- });
29
-
30
- for (const line of result.stdout.split(/\r?\n/)) {
31
- const [commit, subject] = line.split('\0');
32
-
33
- if (subject === message) {
34
- return commit;
35
- }
36
- }
37
-
38
- return undefined;
39
- };
@@ -1,23 +0,0 @@
1
- /**
2
- * Objective: List staged paths.
3
- * Used: When Maestro checks the exact paths staged for a checkpoint.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // Parses Git's NUL-delimited path output.
9
- const parsePaths = (output: string): readonly string[] =>
10
- output.split('\0').filter((path) => path.length > 0);
11
-
12
- // Lists the paths currently staged for commit.
13
- // git diff --cached --name-only -z
14
- export const getStagedPaths = async (
15
- repositoryRoot: string,
16
- ): Promise<readonly string[]> => {
17
- const result = await runGitCommand({
18
- arguments: ['diff', '--cached', '--name-only', '-z'],
19
- cwd: repositoryRoot,
20
- });
21
-
22
- return parsePaths(result.stdout);
23
- };
@@ -1,25 +0,0 @@
1
- /**
2
- * Objective: Read the parent of a commit.
3
- * Used: When Maestro checks a checkpoint parent.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // Gets a commit's direct parent.
9
- // git rev-parse --verify <commit>^
10
- type GetParentCommitInput = {
11
- repositoryRoot: string;
12
- commit: string;
13
- };
14
-
15
- export const getParentCommit = async ({
16
- repositoryRoot,
17
- commit,
18
- }: GetParentCommitInput): Promise<string> => {
19
- const result = await runGitCommand({
20
- arguments: ['rev-parse', '--verify', `${commit}^`],
21
- cwd: repositoryRoot,
22
- });
23
-
24
- return result.stdout.trim();
25
- };
@@ -1,16 +0,0 @@
1
- /**
2
- * Objective: Check that Git can inspect this repository.
3
- * Used: When Maestro checks that Git can inspect a repository.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // git status --porcelain=v1 --untracked-files=no
9
- export async function assertRepositoryTrusted(
10
- repositoryRoot: string,
11
- ): Promise<void> {
12
- await runGitCommand({
13
- arguments: ['status', '--porcelain=v1', '--untracked-files=no'],
14
- cwd: repositoryRoot,
15
- });
16
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Objective: Find the repository root from a working directory.
3
- * Used: When Maestro discovers the Git root from a working directory.
4
- */
5
-
6
- import { realpath } from 'node:fs/promises';
7
- import { runGitCommand } from '#git/command.ts';
8
-
9
- // git rev-parse --path-format=absolute --show-toplevel
10
- export const findRepositoryRoot = async (
11
- cwd: string = process.cwd(),
12
- ): Promise<string> => {
13
- const result = await runGitCommand({
14
- arguments: ['rev-parse', '--path-format=absolute', '--show-toplevel'],
15
- cwd,
16
- });
17
-
18
- return realpath(result.stdout.trim());
19
- };
@@ -1,18 +0,0 @@
1
- /**
2
- * Objective: Read the current Git branch.
3
- * Used: When Maestro reports the branch of the current checkout.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // git symbolic-ref --quiet --short HEAD
9
- export const getCurrentBranch = async (
10
- repositoryRoot: string,
11
- ): Promise<string> => {
12
- const result = await runGitCommand({
13
- arguments: ['symbolic-ref', '--quiet', '--short', 'HEAD'],
14
- cwd: repositoryRoot,
15
- });
16
-
17
- return result.stdout.replace(/\r?\n$/, '');
18
- };
@@ -1,18 +0,0 @@
1
- /**
2
- * Objective: Read the current HEAD commit.
3
- * Used: When Maestro records a workflow commit.
4
- */
5
-
6
- import { runGitCommand } from '#git/command.ts';
7
-
8
- // git rev-parse --verify HEAD^{commit}
9
- export const getHeadCommit = async (
10
- repositoryRoot: string,
11
- ): Promise<string> => {
12
- const result = await runGitCommand({
13
- arguments: ['rev-parse', '--verify', 'HEAD^{commit}'],
14
- cwd: repositoryRoot,
15
- });
16
-
17
- return result.stdout.replace(/\r?\n$/, '');
18
- };
@@ -1,74 +0,0 @@
1
- /**
2
- * Objective: Read all staged, unstaged, and untracked changes in the current checkout.
3
- * Used: When Maestro checks whether the current checkout is clean.
4
- */
5
-
6
- import { type GitCommandResult, runGitCommand } from '#git/command.ts';
7
-
8
- export type RepositoryStatus = {
9
- clean: boolean;
10
- staged: readonly string[];
11
- unstaged: readonly string[];
12
- untracked: readonly string[];
13
- };
14
-
15
- const parseRepositoryStatus = (result: GitCommandResult): RepositoryStatus => {
16
- const staged: string[] = [];
17
- const unstaged: string[] = [];
18
- const untracked: string[] = [];
19
- const records = result.stdout.split('\0');
20
-
21
- for (let index = 0; index < records.length; index += 1) {
22
- const record = records[index];
23
-
24
- if (record.length === 0) {
25
- continue;
26
- }
27
-
28
- const indexStatus = record[0];
29
- const worktreeStatus = record[1];
30
- const path = record.slice(3);
31
-
32
- if (indexStatus === '?' && worktreeStatus === '?') {
33
- untracked.push(path);
34
- continue;
35
- }
36
-
37
- if (indexStatus !== ' ') {
38
- staged.push(path);
39
- }
40
-
41
- if (worktreeStatus !== ' ') {
42
- unstaged.push(path);
43
- }
44
-
45
- if (
46
- indexStatus === 'R' ||
47
- indexStatus === 'C' ||
48
- worktreeStatus === 'R' ||
49
- worktreeStatus === 'C'
50
- ) {
51
- index += 1;
52
- }
53
- }
54
-
55
- return {
56
- clean:
57
- staged.length === 0 && unstaged.length === 0 && untracked.length === 0,
58
- staged,
59
- unstaged,
60
- untracked,
61
- };
62
- };
63
-
64
- // git status --porcelain=v1 --untracked-files=all -z
65
- export const getRepositoryStatus = async (
66
- repositoryRoot: string,
67
- ): Promise<RepositoryStatus> => {
68
- const result = await runGitCommand({
69
- arguments: ['status', '--porcelain=v1', '--untracked-files=all', '-z'],
70
- cwd: repositoryRoot,
71
- });
72
-
73
- return parseRepositoryStatus(result);
74
- };
@@ -1,19 +0,0 @@
1
- /**
2
- * Objective: Identify a Git command failure with a specific exit code.
3
- * Used: By Maestro Git modules when handling unsuccessful commands.
4
- */
5
-
6
- import { GIT_COMMAND_ERROR_CODES, GitCommandError } from '#git/command.ts';
7
-
8
- type HasGitExitCodeInput = {
9
- error: unknown;
10
- exitCode: number;
11
- };
12
-
13
- export const hasGitExitCode = ({
14
- error,
15
- exitCode,
16
- }: HasGitExitCodeInput): boolean =>
17
- error instanceof GitCommandError &&
18
- error.code === GIT_COMMAND_ERROR_CODES.COMMAND_FAILED &&
19
- error.exitCode === exitCode;
@@ -1,41 +0,0 @@
1
- /**
2
- * Objective: Safely replace a file with complete UTF-8 content.
3
- * Used: When Maestro writes state or artifact files.
4
- */
5
-
6
- import { randomUUID } from 'node:crypto';
7
- import { type FileHandle, open, rename, rm } from 'node:fs/promises';
8
- import { basename, dirname, join } from 'node:path';
9
-
10
- const temporaryPathFor = (path: string): string =>
11
- join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
12
-
13
- type WriteAtomicallyInput = {
14
- path: string;
15
- content: string;
16
- };
17
-
18
- export const writeAtomically = async ({
19
- path,
20
- content,
21
- }: WriteAtomicallyInput): Promise<void> => {
22
- const temporaryPath = temporaryPathFor(path);
23
- let file: FileHandle | undefined;
24
-
25
- try {
26
- file = await open(temporaryPath, 'wx', 0o600);
27
- await file.writeFile(content, 'utf8');
28
- await file.sync();
29
- await file.close();
30
- file = undefined;
31
- await rename(temporaryPath, path);
32
- } finally {
33
- try {
34
- if (file !== undefined) {
35
- await file.close();
36
- }
37
- } finally {
38
- await rm(temporaryPath, { force: true });
39
- }
40
- }
41
- };
@@ -1,28 +0,0 @@
1
- /**
2
- * Objective: Serialize JSON and replace its destination atomically.
3
- * Used: When Maestro writes workflow state and artifact JSON files.
4
- */
5
-
6
- import { writeAtomically } from '#utils/write-atomically.ts';
7
-
8
- const jsonContent = (data: unknown): string => {
9
- const content = JSON.stringify(data, null, 2);
10
-
11
- if (content === undefined) {
12
- throw new Error('JSON data must be serializable.');
13
- }
14
-
15
- return `${content}\n`;
16
- };
17
-
18
- type WriteJsonAtomicallyInput = {
19
- path: string;
20
- data: unknown;
21
- };
22
-
23
- export const writeJsonAtomically = async ({
24
- path,
25
- data,
26
- }: WriteJsonAtomicallyInput): Promise<void> => {
27
- await writeAtomically({ path, content: jsonContent(data) });
28
- };