prizmkit 1.1.122 → 1.1.124
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/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +46 -38
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +365 -22
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +118 -109
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +21 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +33 -17
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +109 -63
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +179 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +80 -52
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +348 -154
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +188 -0
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +222 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
package/bundled/VERSION.json
CHANGED
|
@@ -20,6 +20,69 @@ function toClaudePrizmkitCommand(sub) {
|
|
|
20
20
|
return `prizmkit-${normalized}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const REVIEWER_ROLE_SKILL = 'prizmkit-code-review';
|
|
24
|
+
const REVIEWER_ROLE_FILE = 'prizmkit-code-reviewer.md';
|
|
25
|
+
const REVIEWER_REQUIRED_UNAVAILABLE = new Set([
|
|
26
|
+
'workspace-mutation',
|
|
27
|
+
'shell-execution',
|
|
28
|
+
'downstream-execution-creation',
|
|
29
|
+
'delegation-equivalent-behavior',
|
|
30
|
+
'orchestration-reentry',
|
|
31
|
+
'skill-invocation',
|
|
32
|
+
]);
|
|
33
|
+
const CLAUDE_OBSERVATIONAL_TOOLS = new Map([
|
|
34
|
+
['read-files', 'Read'],
|
|
35
|
+
['search-content', 'Grep'],
|
|
36
|
+
['search-paths', 'Glob'],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function buildReviewerRoleDefinition(roleManifest) {
|
|
40
|
+
if (!roleManifest || roleManifest.role_name !== 'prizmkit-code-reviewer') {
|
|
41
|
+
throw new Error('prizmkit-code-review requires assets/reviewer-role.json');
|
|
42
|
+
}
|
|
43
|
+
const unavailable = new Set(roleManifest.unavailable_capabilities || []);
|
|
44
|
+
for (const capability of REVIEWER_REQUIRED_UNAVAILABLE) {
|
|
45
|
+
if (!unavailable.has(capability)) {
|
|
46
|
+
throw new Error(`Reviewer role must make ${capability} unavailable`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (roleManifest.execution_mode !== 'foreground-single-authority'
|
|
50
|
+
|| roleManifest.max_descendant_execution_units !== 0
|
|
51
|
+
|| roleManifest.max_delegation_depth !== 0) {
|
|
52
|
+
throw new Error('Reviewer role violates the single-authority execution topology');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tools = (roleManifest.observational_capabilities || []).map(capability => {
|
|
56
|
+
const tool = CLAUDE_OBSERVATIONAL_TOOLS.get(capability);
|
|
57
|
+
if (!tool) throw new Error(`Unsupported Reviewer observational capability: ${capability}`);
|
|
58
|
+
return tool;
|
|
59
|
+
});
|
|
60
|
+
if (tools.length === 0) throw new Error('Reviewer role requires observational capabilities');
|
|
61
|
+
|
|
62
|
+
return `---
|
|
63
|
+
name: ${roleManifest.role_name}
|
|
64
|
+
description: ${roleManifest.description}
|
|
65
|
+
tools: ${tools.join(', ')}
|
|
66
|
+
permissionMode: dontAsk
|
|
67
|
+
maxTurns: 200
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
You are a capability-restricted read-only Reviewer execution role.
|
|
71
|
+
|
|
72
|
+
Read and follow the complete prompt supplied by the Main Agent. Perform the complete review yourself. Your tool allowlist intentionally excludes workspace mutation, shell execution, skill invocation, downstream execution creation, delegation-equivalent behavior, and orchestration re-entry.
|
|
73
|
+
|
|
74
|
+
If the supplied review cannot be completed with the available observational capabilities and current execution context, return \`REVIEW_INFRASTRUCTURE_BLOCKED\`. Do not delegate, request a helper, mutate the workspace, or claim \`PASS\` from incomplete work.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getClaudeSkillAgentDefinitions(skillName, roleManifest = null) {
|
|
79
|
+
if (skillName !== REVIEWER_ROLE_SKILL) return [];
|
|
80
|
+
return [{
|
|
81
|
+
fileName: REVIEWER_ROLE_FILE,
|
|
82
|
+
content: buildReviewerRoleDefinition(roleManifest),
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
/**
|
|
24
87
|
* Convert a core SKILL.md to Claude Code command.md format.
|
|
25
88
|
* @param {string} skillContent - Content of the core SKILL.md
|
|
@@ -97,6 +160,20 @@ export async function installCommand(corePath, targetRoot) {
|
|
|
97
160
|
for (const subdir of subdirs) {
|
|
98
161
|
cpSync(path.join(corePath, subdir), path.join(targetDir, subdir), { recursive: true });
|
|
99
162
|
}
|
|
163
|
+
|
|
164
|
+
// Install platform-native capability roles generated from canonical skill semantics.
|
|
165
|
+
const roleManifestPath = path.join(corePath, 'assets', 'reviewer-role.json');
|
|
166
|
+
const roleManifest = existsSync(roleManifestPath)
|
|
167
|
+
? JSON.parse(await readFile(roleManifestPath, 'utf8'))
|
|
168
|
+
: null;
|
|
169
|
+
const agentDefinitions = getClaudeSkillAgentDefinitions(skillName, roleManifest);
|
|
170
|
+
if (agentDefinitions.length > 0) {
|
|
171
|
+
const agentsDir = path.join(targetRoot, '.claude', 'agents');
|
|
172
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
173
|
+
for (const definition of agentDefinitions) {
|
|
174
|
+
await writeFile(path.join(agentsDir, definition.fileName), definition.content);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
100
177
|
} else {
|
|
101
178
|
// Single file command
|
|
102
179
|
const targetDir = path.join(targetRoot, COMMANDS_DIR);
|
|
@@ -11,14 +11,20 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
PIPELINE_TRANSIENT_EXCLUDES = (
|
|
15
15
|
":(top,exclude).prizmkit-worktree",
|
|
16
|
-
":(top,exclude).skills-lock.json",
|
|
17
|
-
":(top,exclude).AGENTS.md",
|
|
18
16
|
":(top,exclude,glob).*/worktree",
|
|
19
17
|
":(top,exclude,glob).*/worktree/**",
|
|
20
18
|
":(top,exclude,glob).*/worktrees",
|
|
21
19
|
":(top,exclude,glob).*/worktrees/**",
|
|
20
|
+
":(top,exclude).prizmkit/state",
|
|
21
|
+
":(top,exclude,glob).prizmkit/state/**",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
HIDDEN_TOOL_WORKTREE_EXCLUDES = (
|
|
25
|
+
*PIPELINE_TRANSIENT_EXCLUDES,
|
|
26
|
+
":(top,exclude).skills-lock.json",
|
|
27
|
+
":(top,exclude).AGENTS.md",
|
|
22
28
|
":(top,exclude).claude",
|
|
23
29
|
":(top,exclude,glob).claude/**",
|
|
24
30
|
":(top,exclude).codebuddy",
|
|
@@ -31,12 +37,17 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
|
|
|
31
37
|
":(top,exclude,glob).prizmkit/prizm-docs/**",
|
|
32
38
|
":(top,exclude).prizmkit/config.json",
|
|
33
39
|
":(top,exclude).prizmkit/manifest.json",
|
|
34
|
-
":(top,exclude).prizmkit/state",
|
|
35
|
-
":(top,exclude,glob).prizmkit/state/**",
|
|
36
40
|
":(top,exclude).prizmkit/dev-pipeline",
|
|
37
41
|
":(top,exclude,glob).prizmkit/dev-pipeline/**",
|
|
38
42
|
)
|
|
39
43
|
|
|
44
|
+
UNTRACKED_TRANSIENT_EXCLUDES = (
|
|
45
|
+
*HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
46
|
+
":(top,exclude,glob)**/__pycache__",
|
|
47
|
+
":(top,exclude,glob)**/__pycache__/**",
|
|
48
|
+
":(top,exclude,glob)**/*.py[cod]",
|
|
49
|
+
)
|
|
50
|
+
|
|
40
51
|
PIPELINE_FALLBACK_GIT_NAME = "PrizmKit Pipeline"
|
|
41
52
|
PIPELINE_FALLBACK_GIT_EMAIL = "prizmkit-pipeline@example.invalid"
|
|
42
53
|
|
|
@@ -11,10 +11,37 @@ from collections.abc import Sequence
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from .gitops import
|
|
14
|
+
from .gitops import (
|
|
15
|
+
HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
16
|
+
UNTRACKED_TRANSIENT_EXCLUDES,
|
|
17
|
+
GitCommandResult,
|
|
18
|
+
git_status_safe,
|
|
19
|
+
run_git_command,
|
|
20
|
+
)
|
|
15
21
|
from .runner_models import RunnerFamily
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
LOCAL_ONLY_PLATFORM_PATHS = (
|
|
25
|
+
".claude/settings.local.json",
|
|
26
|
+
".codebuddy/settings.local.json",
|
|
27
|
+
".codex/settings.local.json",
|
|
28
|
+
".agents/settings.local.json",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PreflightWorkspace:
|
|
34
|
+
"""Exact Git paths participating in startup preservation."""
|
|
35
|
+
|
|
36
|
+
tracked: tuple[str, ...]
|
|
37
|
+
visible_untracked: tuple[str, ...]
|
|
38
|
+
unsafe_local: tuple[str, ...]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def preservable(self) -> tuple[str, ...]:
|
|
42
|
+
return _unique_paths((*self.tracked, *self.visible_untracked))
|
|
43
|
+
|
|
44
|
+
|
|
18
45
|
@dataclass(frozen=True)
|
|
19
46
|
class BookkeepingResult:
|
|
20
47
|
"""Result from an optional bookkeeping commit boundary."""
|
|
@@ -42,6 +69,81 @@ def _repo_path(project_root: Path, path: Path) -> str:
|
|
|
42
69
|
return path.as_posix()
|
|
43
70
|
|
|
44
71
|
|
|
72
|
+
def _tracked_status_result(project_root: Path) -> GitCommandResult:
|
|
73
|
+
return run_git_command(project_root, ("status", "--porcelain=v1", "-z", "--untracked-files=no", "--", "."))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _untracked_status_result(project_root: Path) -> GitCommandResult:
|
|
77
|
+
return run_git_command(
|
|
78
|
+
project_root,
|
|
79
|
+
("ls-files", "--others", "--exclude-standard", "-z", "--", ".", *UNTRACKED_TRANSIENT_EXCLUDES),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _unique_paths(paths: Sequence[str]) -> tuple[str, ...]:
|
|
84
|
+
return tuple(dict.fromkeys(path for path in paths if path))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _literal_pathspecs(paths: Sequence[str]) -> tuple[str, ...]:
|
|
88
|
+
return tuple(f":(top,literal){path}" for path in paths)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _status_record_paths(stdout: str) -> tuple[str, ...]:
|
|
92
|
+
"""Decode exact paths from NUL-delimited porcelain-v1 records."""
|
|
93
|
+
records = stdout.split("\0")
|
|
94
|
+
changed: list[str] = []
|
|
95
|
+
index = 0
|
|
96
|
+
while index < len(records):
|
|
97
|
+
record = records[index]
|
|
98
|
+
index += 1
|
|
99
|
+
if not record:
|
|
100
|
+
continue
|
|
101
|
+
if len(record) < 4 or record[2] != " ":
|
|
102
|
+
continue
|
|
103
|
+
status = record[:2]
|
|
104
|
+
changed.append(record[3:])
|
|
105
|
+
if "R" in status or "C" in status:
|
|
106
|
+
if index < len(records) and records[index]:
|
|
107
|
+
changed.append(records[index])
|
|
108
|
+
index += 1
|
|
109
|
+
return _unique_paths(changed)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _preflight_workspace(project_root: Path) -> tuple[PreflightWorkspace | None, GitCommandResult | None]:
|
|
113
|
+
tracked_result = _tracked_status_result(project_root)
|
|
114
|
+
if tracked_result.return_code != 0:
|
|
115
|
+
return None, tracked_result
|
|
116
|
+
untracked_result = _untracked_status_result(project_root)
|
|
117
|
+
if untracked_result.return_code != 0:
|
|
118
|
+
return None, untracked_result
|
|
119
|
+
|
|
120
|
+
tracked = _status_record_paths(tracked_result.stdout)
|
|
121
|
+
visible_untracked = _unique_paths(untracked_result.stdout.split("\0"))
|
|
122
|
+
unsafe_local = tuple(path for path in tracked if path in LOCAL_ONLY_PLATFORM_PATHS)
|
|
123
|
+
return PreflightWorkspace(tracked, visible_untracked, unsafe_local), None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _preflight_failure(message: str) -> GitCommandResult:
|
|
127
|
+
return GitCommandResult(args=("status", "preflight"), return_code=1, stdout="", stderr=message)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _remaining_preflight_changes(project_root: Path) -> GitCommandResult:
|
|
131
|
+
workspace, failure = _preflight_workspace(project_root)
|
|
132
|
+
if failure is not None:
|
|
133
|
+
return failure
|
|
134
|
+
assert workspace is not None
|
|
135
|
+
if workspace.unsafe_local:
|
|
136
|
+
return _preflight_failure(
|
|
137
|
+
"Refusing to auto-commit local-only platform settings: " + ", ".join(workspace.unsafe_local)
|
|
138
|
+
)
|
|
139
|
+
return GitCommandResult(
|
|
140
|
+
args=("status", "preflight"),
|
|
141
|
+
return_code=0,
|
|
142
|
+
stdout="\0".join(workspace.preservable),
|
|
143
|
+
stderr="",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
45
147
|
def _visible_status_result(project_root: Path) -> GitCommandResult:
|
|
46
148
|
return run_git_command(project_root, ("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
47
149
|
|
|
@@ -85,23 +187,33 @@ def commit_bookkeeping_changes(
|
|
|
85
187
|
|
|
86
188
|
|
|
87
189
|
def commit_preflight_ready_changes(project_root: Path, item_id: str) -> BookkeepingResult:
|
|
88
|
-
"""Preserve visible
|
|
89
|
-
|
|
90
|
-
if
|
|
91
|
-
return BookkeepingResult(attempted=True, committed=False, result=
|
|
92
|
-
|
|
190
|
+
"""Preserve tracked changes and visible untracked files before task branch setup."""
|
|
191
|
+
workspace, failure = _preflight_workspace(project_root)
|
|
192
|
+
if failure is not None:
|
|
193
|
+
return BookkeepingResult(attempted=True, committed=False, result=failure)
|
|
194
|
+
assert workspace is not None
|
|
195
|
+
if workspace.unsafe_local:
|
|
196
|
+
return BookkeepingResult(
|
|
197
|
+
attempted=True,
|
|
198
|
+
committed=False,
|
|
199
|
+
result=_preflight_failure(
|
|
200
|
+
"Refusing to auto-commit local-only platform settings: " + ", ".join(workspace.unsafe_local)
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
paths = workspace.preservable
|
|
93
204
|
if not paths:
|
|
94
205
|
return BookkeepingResult(attempted=False, committed=False)
|
|
95
|
-
|
|
206
|
+
pathspecs = _literal_pathspecs(paths)
|
|
207
|
+
add_result = run_git_command(project_root, ("add", "-A", "-f", "--", *pathspecs))
|
|
96
208
|
if add_result.return_code != 0:
|
|
97
209
|
return BookkeepingResult(attempted=True, committed=False, result=add_result)
|
|
98
210
|
result = run_git_command(
|
|
99
211
|
project_root,
|
|
100
|
-
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", *
|
|
212
|
+
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", *pathspecs),
|
|
101
213
|
)
|
|
102
214
|
if result.return_code != 0:
|
|
103
215
|
return BookkeepingResult(attempted=True, committed=False, result=result)
|
|
104
|
-
clean_result =
|
|
216
|
+
clean_result = _remaining_preflight_changes(project_root)
|
|
105
217
|
if clean_result.return_code != 0 or clean_result.stdout.strip():
|
|
106
218
|
return BookkeepingResult(attempted=True, committed=False, result=clean_result)
|
|
107
219
|
return BookkeepingResult(attempted=True, committed=True, result=result)
|
|
@@ -107,31 +107,39 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
107
107
|
from .runner_models import PipelineRunResult
|
|
108
108
|
|
|
109
109
|
processed = 0
|
|
110
|
-
|
|
110
|
+
item_statuses: dict[str, str] = {}
|
|
111
111
|
if invocation.item_id:
|
|
112
112
|
try:
|
|
113
113
|
status = _process_item(family, invocation, invocation.item_id, paths, initial_metadata={})
|
|
114
114
|
except KeyboardInterrupt:
|
|
115
|
-
return PipelineRunResult(False, 1, "interrupted", "interrupted"
|
|
116
|
-
return PipelineRunResult(status == "completed", 1, "single_item_done", status
|
|
115
|
+
return PipelineRunResult(False, 1, "interrupted", "interrupted")
|
|
116
|
+
return PipelineRunResult(status == "completed", 1, "single_item_done", status)
|
|
117
117
|
|
|
118
118
|
while True:
|
|
119
119
|
next_result = get_next(family, invocation, paths.project_root)
|
|
120
|
+
details = _render_item_statuses(item_statuses)
|
|
120
121
|
if not next_result.ok:
|
|
121
122
|
return PipelineRunResult(False, processed, "get_next_failed", details=(next_result.text,))
|
|
122
123
|
marker = next_result.stdout.strip()
|
|
123
124
|
if marker == "PIPELINE_COMPLETE":
|
|
124
|
-
|
|
125
|
+
blocking_statuses = _blocking_statuses(family, item_statuses)
|
|
126
|
+
if blocking_statuses:
|
|
125
127
|
return PipelineRunResult(
|
|
126
128
|
False,
|
|
127
129
|
processed,
|
|
128
130
|
"pipeline_complete_with_non_terminal_history",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
blocking_statuses[-1],
|
|
132
|
+
details,
|
|
131
133
|
)
|
|
132
|
-
return PipelineRunResult(
|
|
134
|
+
return PipelineRunResult(
|
|
135
|
+
True,
|
|
136
|
+
processed,
|
|
137
|
+
"pipeline_complete",
|
|
138
|
+
_last_item_status(item_statuses),
|
|
139
|
+
details,
|
|
140
|
+
)
|
|
133
141
|
if marker == "PIPELINE_BLOCKED":
|
|
134
|
-
return PipelineRunResult(False, processed, "pipeline_blocked", details=
|
|
142
|
+
return PipelineRunResult(False, processed, "pipeline_blocked", details=details)
|
|
135
143
|
if not isinstance(next_result.data, dict):
|
|
136
144
|
return PipelineRunResult(False, processed, "invalid_get_next_output", details=(marker[:500],))
|
|
137
145
|
item_id = str(next_result.data.get(f"{family.kind}_id") or next_result.data.get("feature_id") or next_result.data.get("bug_id") or next_result.data.get("refactor_id") or "")
|
|
@@ -141,50 +149,50 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
141
149
|
final_status = _process_item(family, invocation, item_id, paths, initial_metadata=next_result.data)
|
|
142
150
|
except KeyboardInterrupt:
|
|
143
151
|
processed += 1
|
|
144
|
-
|
|
145
|
-
return PipelineRunResult(
|
|
152
|
+
item_statuses[item_id] = "interrupted"
|
|
153
|
+
return PipelineRunResult(
|
|
154
|
+
False,
|
|
155
|
+
processed,
|
|
156
|
+
"interrupted",
|
|
157
|
+
"interrupted",
|
|
158
|
+
_render_item_statuses(item_statuses),
|
|
159
|
+
)
|
|
146
160
|
processed += 1
|
|
147
|
-
|
|
148
|
-
if final_status
|
|
149
|
-
|
|
161
|
+
item_statuses[item_id] = final_status
|
|
162
|
+
if final_status in _completion_compatible_statuses(family):
|
|
163
|
+
if final_status == "completed":
|
|
164
|
+
_emit_info(f"Pausing 5s before next {family.kind}...")
|
|
150
165
|
continue
|
|
151
166
|
if final_status == "pending":
|
|
152
167
|
continue
|
|
168
|
+
details = _render_item_statuses(item_statuses)
|
|
153
169
|
if RunnerEnvironment.from_env().stop_on_failure:
|
|
154
|
-
return PipelineRunResult(False, processed, "stop_on_failure", final_status,
|
|
170
|
+
return PipelineRunResult(False, processed, "stop_on_failure", final_status, details)
|
|
155
171
|
if final_status in {"failed", "needs_info"}:
|
|
156
|
-
return PipelineRunResult(False, processed, "item_failed", final_status,
|
|
172
|
+
return PipelineRunResult(False, processed, "item_failed", final_status, details)
|
|
173
|
+
|
|
157
174
|
|
|
175
|
+
_COMPLETION_COMPATIBLE_STATUSES = {"completed", "skipped", "auto_skipped"}
|
|
158
176
|
|
|
159
|
-
_NON_TERMINAL_DETAIL_STATUSES = {
|
|
160
|
-
"pending",
|
|
161
|
-
"retryable",
|
|
162
|
-
"in_progress",
|
|
163
|
-
"infra_error",
|
|
164
|
-
"context_overflow",
|
|
165
|
-
"stalled_context_continuation",
|
|
166
|
-
"crashed",
|
|
167
|
-
"timed_out",
|
|
168
|
-
"failed",
|
|
169
|
-
"needs_info",
|
|
170
|
-
"merge_conflict",
|
|
171
|
-
"interrupted",
|
|
172
|
-
"unknown",
|
|
173
|
-
}
|
|
174
177
|
|
|
178
|
+
def _completion_compatible_statuses(family: RunnerFamily) -> set[str]:
|
|
179
|
+
statuses = set(_COMPLETION_COMPATIBLE_STATUSES)
|
|
180
|
+
if family.kind == "feature":
|
|
181
|
+
statuses.add("split")
|
|
182
|
+
return statuses
|
|
175
183
|
|
|
176
|
-
def _detail_status(detail: str) -> str:
|
|
177
|
-
return detail.rsplit(":", 1)[-1] if ":" in detail else detail
|
|
178
184
|
|
|
185
|
+
def _render_item_statuses(item_statuses: Mapping[str, str]) -> tuple[str, ...]:
|
|
186
|
+
return tuple(f"{item_id}:{status}" for item_id, status in item_statuses.items())
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return _detail_status(details[-1])
|
|
188
|
+
|
|
189
|
+
def _last_item_status(item_statuses: Mapping[str, str]) -> str:
|
|
190
|
+
return next(reversed(item_statuses.values()), "")
|
|
184
191
|
|
|
185
192
|
|
|
186
|
-
def
|
|
187
|
-
|
|
193
|
+
def _blocking_statuses(family: RunnerFamily, item_statuses: Mapping[str, str]) -> tuple[str, ...]:
|
|
194
|
+
compatible = _completion_compatible_statuses(family)
|
|
195
|
+
return tuple(status for status in item_statuses.values() if status not in compatible)
|
|
188
196
|
|
|
189
197
|
|
|
190
198
|
_SEPARATOR = "─" * 52
|