@wichayutdew/pi-workflows 1.0.1 → 2.0.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.
- package/README.md +312 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -84
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +3 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
2
|
-
import { tokenizeRestrictedCommand } from './restricted-command.ts';
|
|
3
|
-
import { parseRestrictedGitCommand } from './restricted-git.ts';
|
|
4
|
-
|
|
5
|
-
const SHELL_WRAPPERS: ReadonlySet<string> = new Set([
|
|
6
|
-
'bash',
|
|
7
|
-
'env',
|
|
8
|
-
'exec',
|
|
9
|
-
'fish',
|
|
10
|
-
'sh',
|
|
11
|
-
'xargs',
|
|
12
|
-
'zsh',
|
|
13
|
-
]);
|
|
14
|
-
const REMOTE_EXECUTABLES: ReadonlySet<string> = new Set([
|
|
15
|
-
'curl',
|
|
16
|
-
'scp',
|
|
17
|
-
'ssh',
|
|
18
|
-
'rsync',
|
|
19
|
-
'wget',
|
|
20
|
-
]);
|
|
21
|
-
const FORBIDDEN_LONG_PUSH_OPTIONS = [
|
|
22
|
-
'--force',
|
|
23
|
-
'--force-if-includes',
|
|
24
|
-
'--force-with-lease',
|
|
25
|
-
'--all',
|
|
26
|
-
'--delete',
|
|
27
|
-
'--mirror',
|
|
28
|
-
'--prune',
|
|
29
|
-
'--tags',
|
|
30
|
-
] as const;
|
|
31
|
-
const PUBLISH_EXECUTABLES: ReadonlySet<string> = new Set([
|
|
32
|
-
'bun',
|
|
33
|
-
'cargo',
|
|
34
|
-
'npm',
|
|
35
|
-
'pnpm',
|
|
36
|
-
'yarn',
|
|
37
|
-
]);
|
|
38
|
-
const LOCAL_VERIFICATION_GIT_SUBCOMMANDS: ReadonlySet<string> = new Set([
|
|
39
|
-
'add',
|
|
40
|
-
'branch',
|
|
41
|
-
'commit',
|
|
42
|
-
'diff',
|
|
43
|
-
'grep',
|
|
44
|
-
'log',
|
|
45
|
-
'ls-files',
|
|
46
|
-
'rev-parse',
|
|
47
|
-
'show',
|
|
48
|
-
'status',
|
|
49
|
-
'worktree',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const containsPublishOperation = (tokens: ReadonlyArray<string>): boolean =>
|
|
53
|
-
tokens
|
|
54
|
-
.slice(1)
|
|
55
|
-
.some((token) => token.length >= 3 && 'publish'.startsWith(token));
|
|
56
|
-
|
|
57
|
-
const hasDestructivePushShortOption = (token: string): boolean =>
|
|
58
|
-
token.startsWith('-') &&
|
|
59
|
-
!token.startsWith('--') &&
|
|
60
|
-
(token.slice(1).includes('f') || token.slice(1).includes('d'));
|
|
61
|
-
|
|
62
|
-
const hasForbiddenLongPushOption = (token: string): boolean => {
|
|
63
|
-
if (!token.startsWith('--') || token === '--') return false;
|
|
64
|
-
const optionName = token.split('=', 1)[0] ?? token;
|
|
65
|
-
return FORBIDDEN_LONG_PUSH_OPTIONS.some((option) =>
|
|
66
|
-
option.startsWith(optionName),
|
|
67
|
-
);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const hasEmptyPushRefspecSide = (token: string): boolean => {
|
|
71
|
-
const separator = token.indexOf(':');
|
|
72
|
-
return separator >= 0 && (separator === 0 || separator === token.length - 1);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Determines whether a reviewed verification command is safe to authorize.
|
|
77
|
-
*
|
|
78
|
-
* @param command - Exact command extracted from an approved artifact.
|
|
79
|
-
* @returns `true` for local, non-publishing verification commands.
|
|
80
|
-
*/
|
|
81
|
-
export const isSafeVerificationCommand = (command: string): boolean => {
|
|
82
|
-
const parsed = tokenizeRestrictedCommand(command);
|
|
83
|
-
if (!parsed.tokens) return false;
|
|
84
|
-
|
|
85
|
-
const executable = basename(parsed.tokens[0] ?? '');
|
|
86
|
-
if (SHELL_WRAPPERS.has(executable) || REMOTE_EXECUTABLES.has(executable)) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const subcommand =
|
|
91
|
-
executable === 'git'
|
|
92
|
-
? parseRestrictedGitCommand(parsed.tokens)?.subcommand
|
|
93
|
-
: parsed.tokens[1];
|
|
94
|
-
if (
|
|
95
|
-
executable === 'git' &&
|
|
96
|
-
(!subcommand || !LOCAL_VERIFICATION_GIT_SUBCOMMANDS.has(subcommand))
|
|
97
|
-
) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
if (executable === 'gh' || executable === 'glab') return false;
|
|
101
|
-
if (
|
|
102
|
-
PUBLISH_EXECUTABLES.has(executable) &&
|
|
103
|
-
containsPublishOperation(parsed.tokens)
|
|
104
|
-
) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
return !(executable === 'docker' && parsed.tokens.slice(1).includes('push'));
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const isHostedApiDelete = (tokens: ReadonlyArray<string>): boolean =>
|
|
111
|
-
tokens.slice(2).some((token, index, apiTokens) => {
|
|
112
|
-
const normalizedToken = token.toUpperCase();
|
|
113
|
-
return (
|
|
114
|
-
normalizedToken === '--METHOD=DELETE' ||
|
|
115
|
-
normalizedToken === '-XDELETE' ||
|
|
116
|
-
((normalizedToken === '--METHOD' || normalizedToken === '-X') &&
|
|
117
|
-
apiTokens[index + 1]?.toUpperCase() === 'DELETE')
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Determines whether a reviewed remote action is narrowly safe to authorize.
|
|
123
|
-
*
|
|
124
|
-
* @param command - Exact command extracted from an approved artifact.
|
|
125
|
-
* @returns `true` for non-destructive Git pushes and hosted API writes.
|
|
126
|
-
*/
|
|
127
|
-
export const isSafeRemoteActionCommand = (command: string): boolean => {
|
|
128
|
-
const parsed = tokenizeRestrictedCommand(command);
|
|
129
|
-
if (!parsed.tokens) return false;
|
|
130
|
-
|
|
131
|
-
const executable = parsed.tokens[0];
|
|
132
|
-
const subcommand =
|
|
133
|
-
executable === 'git'
|
|
134
|
-
? parseRestrictedGitCommand(parsed.tokens)?.subcommand
|
|
135
|
-
: parsed.tokens[1];
|
|
136
|
-
if (executable === 'gh' || executable === 'glab') {
|
|
137
|
-
return subcommand === 'api' && !isHostedApiDelete(parsed.tokens);
|
|
138
|
-
}
|
|
139
|
-
if (executable !== 'git' || subcommand !== 'push') return false;
|
|
140
|
-
|
|
141
|
-
return !parsed.tokens
|
|
142
|
-
.slice(1)
|
|
143
|
-
.some(
|
|
144
|
-
(token) =>
|
|
145
|
-
hasForbiddenLongPushOption(token) ||
|
|
146
|
-
token.startsWith('+') ||
|
|
147
|
-
hasEmptyPushRefspecSide(token) ||
|
|
148
|
-
hasDestructivePushShortOption(token),
|
|
149
|
-
);
|
|
150
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
2
|
-
import { tokenizeRestrictedCommand } from './restricted-command.ts';
|
|
3
|
-
import {
|
|
4
|
-
parseJsonDocuments,
|
|
5
|
-
verificationCommands,
|
|
6
|
-
} from './reviewed-artifact.ts';
|
|
7
|
-
|
|
8
|
-
const malformedBunInstallReason = (command: string): string | undefined => {
|
|
9
|
-
const parsed = tokenizeRestrictedCommand(command);
|
|
10
|
-
if (!parsed.tokens || basename(parsed.tokens[0] ?? '') !== 'bun') {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const installIndex = parsed.tokens.indexOf('install', 1);
|
|
15
|
-
if (installIndex <= 1) return undefined;
|
|
16
|
-
const hasCwdBeforeInstall = parsed.tokens
|
|
17
|
-
.slice(1, installIndex)
|
|
18
|
-
.some((token) => token === '--cwd' || token.startsWith('--cwd='));
|
|
19
|
-
if (!hasCwdBeforeInstall) return undefined;
|
|
20
|
-
|
|
21
|
-
return [
|
|
22
|
-
`Invalid Bun install command: ${JSON.stringify(command)}.`,
|
|
23
|
-
'`--cwd` appears before `install`, so Bun interprets `install` as a package script.',
|
|
24
|
-
'Use `bun install --cwd <absolute-cwd> --frozen-lockfile`, preserving the reviewed path and any other intended install flags, then resubmit the plan.',
|
|
25
|
-
].join(' ');
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Rejects known command-shape mistakes before human approval.
|
|
30
|
-
*
|
|
31
|
-
* Deterministic checks prevent known parser traps from becoming approved
|
|
32
|
-
* capabilities while leaving arbitrary runtime diagnosis to the agent.
|
|
33
|
-
*
|
|
34
|
-
* @param artifact - Reviewed execution contract.
|
|
35
|
-
* @returns A corrective error message, or `undefined` when shapes are valid.
|
|
36
|
-
*/
|
|
37
|
-
export const reviewedCommandShapeError = (
|
|
38
|
-
artifact: string,
|
|
39
|
-
): string | undefined => {
|
|
40
|
-
for (const document of parseJsonDocuments(artifact)) {
|
|
41
|
-
for (const role of ['worker', 'reviewer'] as const) {
|
|
42
|
-
for (const command of verificationCommands(document, role)) {
|
|
43
|
-
const reason = malformedBunInstallReason(command);
|
|
44
|
-
if (reason) return reason;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return undefined;
|
|
49
|
-
};
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { isAbsolute } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
isRecord,
|
|
4
|
-
parseJsonDocumentsWithValidity,
|
|
5
|
-
} from './reviewed-artifact.ts';
|
|
6
|
-
|
|
7
|
-
export type ReviewedRepositoryContract =
|
|
8
|
-
| { readonly kind: 'none' }
|
|
9
|
-
| { readonly kind: 'invalid'; readonly reason: string }
|
|
10
|
-
| {
|
|
11
|
-
readonly kind: 'valid';
|
|
12
|
-
readonly repositoryCwd: string;
|
|
13
|
-
readonly sourceCwd?: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const invalidContract = (reason: string): ReviewedRepositoryContract => ({
|
|
17
|
-
kind: 'invalid',
|
|
18
|
-
reason,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const isAbsoluteSafePath = (value: unknown): value is string =>
|
|
22
|
-
typeof value === 'string' && isAbsolute(value) && !value.includes('\0');
|
|
23
|
-
|
|
24
|
-
type RepositoryDirectories = {
|
|
25
|
-
readonly repositoryDirectories: ReadonlySet<string>;
|
|
26
|
-
readonly sourceDirectories: ReadonlySet<string>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const addRepositoryDirectories = (
|
|
30
|
-
document: Readonly<Record<string, unknown>>,
|
|
31
|
-
repositoryDirectories: Set<string>,
|
|
32
|
-
sourceDirectories: Set<string>,
|
|
33
|
-
): ReviewedRepositoryContract | undefined => {
|
|
34
|
-
if (
|
|
35
|
-
!Array.isArray(document.repositories) ||
|
|
36
|
-
document.repositories.length === 0
|
|
37
|
-
) {
|
|
38
|
-
return invalidContract(
|
|
39
|
-
'Reviewed repository contract must contain a non-empty repositories array',
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const repository of document.repositories) {
|
|
44
|
-
if (!isRecord(repository)) {
|
|
45
|
-
return invalidContract(
|
|
46
|
-
'Reviewed repository contract contains a malformed repository entry',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (!isAbsoluteSafePath(repository.cwd)) {
|
|
50
|
-
return invalidContract(
|
|
51
|
-
'Reviewed repository contract repository cwd must be an absolute path',
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
repositoryDirectories.add(repository.cwd);
|
|
55
|
-
|
|
56
|
-
if ('sourceCwd' in repository) {
|
|
57
|
-
if (!isAbsoluteSafePath(repository.sourceCwd)) {
|
|
58
|
-
return invalidContract(
|
|
59
|
-
'Reviewed repository contract sourceCwd must be an absolute path',
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
sourceDirectories.add(repository.sourceCwd);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const collectRepositoryDirectories = (
|
|
69
|
-
documents: ReadonlyArray<unknown>,
|
|
70
|
-
): RepositoryDirectories | ReviewedRepositoryContract => {
|
|
71
|
-
const repositoryDirectories = new Set<string>();
|
|
72
|
-
const sourceDirectories = new Set<string>();
|
|
73
|
-
|
|
74
|
-
for (const document of documents) {
|
|
75
|
-
if (!isRecord(document) || !('repositories' in document)) continue;
|
|
76
|
-
const error = addRepositoryDirectories(
|
|
77
|
-
document,
|
|
78
|
-
repositoryDirectories,
|
|
79
|
-
sourceDirectories,
|
|
80
|
-
);
|
|
81
|
-
if (error) return error;
|
|
82
|
-
}
|
|
83
|
-
return { repositoryDirectories, sourceDirectories };
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Parses and validates the repository-directory portion of a reviewed artifact.
|
|
88
|
-
*
|
|
89
|
-
* This function is the pure validation core; filesystem availability is
|
|
90
|
-
* resolved separately.
|
|
91
|
-
*
|
|
92
|
-
* @param artifact - Human-reviewed artifact text.
|
|
93
|
-
* @returns A validated repository contract, absence, or validation error.
|
|
94
|
-
*/
|
|
95
|
-
export const parseReviewedRepositoryContract = (
|
|
96
|
-
artifact: string,
|
|
97
|
-
): ReviewedRepositoryContract => {
|
|
98
|
-
const parsed = parseJsonDocumentsWithValidity(artifact);
|
|
99
|
-
const hasRepositoryContract = parsed.documents.some(
|
|
100
|
-
(document) => isRecord(document) && 'repositories' in document,
|
|
101
|
-
);
|
|
102
|
-
if (!hasRepositoryContract) {
|
|
103
|
-
return parsed.hasMalformedCandidate
|
|
104
|
-
? invalidContract('Reviewed repository contract contains malformed JSON')
|
|
105
|
-
: { kind: 'none' };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const collected = collectRepositoryDirectories(parsed.documents);
|
|
109
|
-
if ('kind' in collected) return collected;
|
|
110
|
-
if (parsed.hasMalformedCandidate) {
|
|
111
|
-
return invalidContract(
|
|
112
|
-
'Reviewed repository contract contains malformed JSON',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
if (collected.repositoryDirectories.size !== 1) {
|
|
116
|
-
return invalidContract(
|
|
117
|
-
'Reviewed repository contract is ambiguous: expected exactly one repository cwd',
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
if (collected.sourceDirectories.size > 1) {
|
|
121
|
-
return invalidContract(
|
|
122
|
-
'Reviewed repository contract is ambiguous: expected at most one sourceCwd',
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const [repositoryCwd] = collected.repositoryDirectories;
|
|
127
|
-
if (!repositoryCwd) {
|
|
128
|
-
return invalidContract(
|
|
129
|
-
'Reviewed repository contract is ambiguous: expected exactly one repository cwd',
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const [sourceCwd] = collected.sourceDirectories;
|
|
133
|
-
return {
|
|
134
|
-
kind: 'valid',
|
|
135
|
-
repositoryCwd,
|
|
136
|
-
...(sourceCwd ? { sourceCwd } : {}),
|
|
137
|
-
};
|
|
138
|
-
};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { statSync } from 'node:fs';
|
|
2
|
-
import { isRecord } from './reviewed-artifact.ts';
|
|
3
|
-
import { parseReviewedRepositoryContract } from './reviewed-repository-contract.ts';
|
|
4
|
-
|
|
5
|
-
export type ReviewedRepositoryCwdResolution =
|
|
6
|
-
| { readonly kind: 'none' }
|
|
7
|
-
| { readonly kind: 'invalid'; readonly reason: string }
|
|
8
|
-
| {
|
|
9
|
-
readonly kind: 'resolved';
|
|
10
|
-
readonly cwd: string;
|
|
11
|
-
readonly repositoryCwd: string;
|
|
12
|
-
readonly bootstrapping: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type DirectoryState = 'directory' | 'missing' | 'invalid';
|
|
16
|
-
|
|
17
|
-
export type ReviewedRepositoryCwdDependencies = {
|
|
18
|
-
readonly readDirectoryState: (path: string) => DirectoryState;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type ResolveReviewedRepositoryCwd = (
|
|
22
|
-
artifact: string,
|
|
23
|
-
) => ReviewedRepositoryCwdResolution;
|
|
24
|
-
|
|
25
|
-
const readDirectoryState = (path: string): DirectoryState => {
|
|
26
|
-
try {
|
|
27
|
-
return statSync(path).isDirectory() ? 'directory' : 'invalid';
|
|
28
|
-
} catch (error) {
|
|
29
|
-
const code = isRecord(error) ? error.code : undefined;
|
|
30
|
-
return code === 'ENOENT' || code === 'ENOTDIR' ? 'missing' : 'invalid';
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const invalidResolution = (
|
|
35
|
-
reason: string,
|
|
36
|
-
): ReviewedRepositoryCwdResolution => ({
|
|
37
|
-
kind: 'invalid',
|
|
38
|
-
reason,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Creates a reviewed-directory resolver with an injected filesystem reader.
|
|
43
|
-
*
|
|
44
|
-
* The injected boundary keeps contract parsing and launch-path selection
|
|
45
|
-
* deterministic and independently testable.
|
|
46
|
-
*
|
|
47
|
-
* @param dependencies - Filesystem operations used by the resolver.
|
|
48
|
-
* @returns A resolver bound to those dependencies.
|
|
49
|
-
*/
|
|
50
|
-
export const createReviewedRepositoryCwdResolver = (
|
|
51
|
-
dependencies: ReviewedRepositoryCwdDependencies,
|
|
52
|
-
): ResolveReviewedRepositoryCwd => {
|
|
53
|
-
return (artifact): ReviewedRepositoryCwdResolution => {
|
|
54
|
-
const contract = parseReviewedRepositoryContract(artifact);
|
|
55
|
-
if (contract.kind !== 'valid') return contract;
|
|
56
|
-
|
|
57
|
-
const repositoryState = dependencies.readDirectoryState(
|
|
58
|
-
contract.repositoryCwd,
|
|
59
|
-
);
|
|
60
|
-
if (repositoryState === 'directory') {
|
|
61
|
-
return {
|
|
62
|
-
kind: 'resolved',
|
|
63
|
-
cwd: contract.repositoryCwd,
|
|
64
|
-
repositoryCwd: contract.repositoryCwd,
|
|
65
|
-
bootstrapping: false,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (repositoryState === 'invalid') {
|
|
69
|
-
return invalidResolution(
|
|
70
|
-
`Reviewed repository cwd is not an accessible directory: ${contract.repositoryCwd}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
if (!contract.sourceCwd) {
|
|
74
|
-
return invalidResolution(
|
|
75
|
-
'Reviewed repository target is missing and requires exactly one absolute sourceCwd',
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (dependencies.readDirectoryState(contract.sourceCwd) !== 'directory') {
|
|
79
|
-
return invalidResolution(
|
|
80
|
-
`Reviewed repository sourceCwd is not an existing directory: ${contract.sourceCwd}`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
kind: 'resolved',
|
|
85
|
-
cwd: contract.sourceCwd,
|
|
86
|
-
repositoryCwd: contract.repositoryCwd,
|
|
87
|
-
bootstrapping: true,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Resolves the launch directory from a reviewed repository contract.
|
|
94
|
-
*
|
|
95
|
-
* Malformed, ambiguous, or incomplete contracts never fall back silently.
|
|
96
|
-
*
|
|
97
|
-
* @param artifact - Human-reviewed artifact text.
|
|
98
|
-
* @returns The resolved directory, absence, or a validation error.
|
|
99
|
-
*/
|
|
100
|
-
export const resolveReviewedRepositoryCwd = createReviewedRepositoryCwdResolver(
|
|
101
|
-
{ readDirectoryState },
|
|
102
|
-
);
|