@magnusekdahl/parallix 1.2.1 → 1.3.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.
- package/index.js +190 -185
- package/lib/agents/agents.js +34 -4
- package/lib/agents/agents.ts +40 -5
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1331 -1468
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +4 -4
- package/px.js +226 -208
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Review event persistence for autonomous review rounds.
|
|
3
4
|
*
|
|
@@ -9,1050 +10,707 @@
|
|
|
9
10
|
*
|
|
10
11
|
* Owned by the Node workflow harness (task-1145).
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.LEGACY_ARTIFACT_TO_EVENT_TYPE = exports.VALID_VERDICTS = exports.VALID_DISPOSITIONS = exports.MIRRORED_EVENT_TYPES = exports.ALL_EVENT_TYPES = exports.VALID_EVENT_TYPES = void 0;
|
|
48
|
+
exports.shouldMirrorToForgejo = shouldMirrorToProvider;
|
|
49
|
+
exports.shouldMirrorToProvider = shouldMirrorToProvider;
|
|
50
|
+
exports.isValidEventType = isValidEventType;
|
|
51
|
+
exports.isValidDisposition = isValidDisposition;
|
|
52
|
+
exports.isValidVerdict = isValidVerdict;
|
|
53
|
+
exports.reviewEventsDir = reviewEventsDir;
|
|
54
|
+
exports.eventFilePath = eventFilePath;
|
|
55
|
+
exports.legacyArtifactPath = legacyArtifactPath;
|
|
56
|
+
exports.createEvent = createEvent;
|
|
57
|
+
exports.normalizeEventContent = normalizeEventContent;
|
|
58
|
+
exports.importLegacyArtifact = importLegacyArtifact;
|
|
59
|
+
exports.importAllLegacyArtifacts = importAllLegacyArtifacts;
|
|
60
|
+
exports.readAllEvents = readAllEvents;
|
|
61
|
+
exports.parseEventFile = parseEventFile;
|
|
62
|
+
exports.buildEventFrontmatter = buildEventFrontmatter;
|
|
63
|
+
exports.buildEventFooter = buildEventFooter;
|
|
64
|
+
exports.renderEventFile = renderEventFile;
|
|
65
|
+
exports.hasWorkflowFooter = hasWorkflowFooter;
|
|
66
|
+
exports.classifyComment = classifyComment;
|
|
67
|
+
exports.consumeHumanNotes = consumeHumanNotes;
|
|
68
|
+
exports.generateEventTimestamp = generateEventTimestamp;
|
|
69
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
70
|
+
const fs = __importStar(require("fs"));
|
|
71
|
+
const path = __importStar(require("path"));
|
|
72
|
+
const os = __importStar(require("os"));
|
|
73
|
+
const git_js_1 = require("../core/git.js");
|
|
74
|
+
const mission_utils_js_1 = require("../core/mission-utils.js");
|
|
75
|
+
const review_state_js_1 = require("./review-state.js");
|
|
76
|
+
const fmt = __importStar(require("../core/fmt.js"));
|
|
21
77
|
// -------- Event Taxonomy --------
|
|
22
|
-
|
|
23
78
|
/**
|
|
24
79
|
* Valid event classification labels.
|
|
25
80
|
* These are the canonical types that can be stored and (optionally) mirrored to the review provider.
|
|
26
81
|
*/
|
|
27
82
|
const VALID_EVENT_TYPES = Object.freeze({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Workflow-internal events (not mirrored to the review provider by default)
|
|
41
|
-
BLOCKED_PUBLICATION: 'blocked_publication',
|
|
42
|
-
PARKED_FOLLOWUP: 'parked_followup'
|
|
83
|
+
// Reviewer-produced events
|
|
84
|
+
REVIEWER_FINDINGS: 'reviewer_findings',
|
|
85
|
+
REVIEWER_OUTCOME: 'reviewer_outcome',
|
|
86
|
+
// Implementer-produced events
|
|
87
|
+
IMPLEMENTER_ROUND_SUMMARY: 'implementer_round_summary',
|
|
88
|
+
IMPLEMENTER_DISPOSITION: 'implementer_disposition',
|
|
89
|
+
// Neutral events
|
|
90
|
+
NEUTRAL_DISCUSSION: 'neutral_discussion',
|
|
91
|
+
HUMAN_NOTE: 'human_note',
|
|
92
|
+
// Workflow-internal events (not mirrored to the review provider by default)
|
|
93
|
+
BLOCKED_PUBLICATION: 'blocked_publication',
|
|
94
|
+
PARKED_FOLLOWUP: 'parked_followup'
|
|
43
95
|
});
|
|
44
|
-
|
|
96
|
+
exports.VALID_EVENT_TYPES = VALID_EVENT_TYPES;
|
|
45
97
|
/**
|
|
46
98
|
* All valid event type values as an array for validation.
|
|
47
99
|
*/
|
|
48
100
|
const ALL_EVENT_TYPES = Object.freeze(Object.values(VALID_EVENT_TYPES));
|
|
49
|
-
|
|
101
|
+
exports.ALL_EVENT_TYPES = ALL_EVENT_TYPES;
|
|
50
102
|
/**
|
|
51
103
|
* Event types that should be mirrored to the review provider.
|
|
52
|
-
* Reviewer findings, reviewer outcome, implementer round summary, implementer disposition,
|
|
53
|
-
* and round-affecting human notes are mirrored.
|
|
54
|
-
* Raw draft artifacts, retry bookkeeping, classification metadata, and local diagnostics
|
|
55
|
-
* remain workflow-local by default.
|
|
56
104
|
*/
|
|
57
105
|
const MIRRORED_EVENT_TYPES = Object.freeze(new Set([
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
VALID_EVENT_TYPES.REVIEWER_FINDINGS,
|
|
107
|
+
VALID_EVENT_TYPES.REVIEWER_OUTCOME,
|
|
108
|
+
VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY,
|
|
109
|
+
VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION,
|
|
110
|
+
VALID_EVENT_TYPES.HUMAN_NOTE
|
|
63
111
|
]));
|
|
64
|
-
|
|
112
|
+
exports.MIRRORED_EVENT_TYPES = MIRRORED_EVENT_TYPES;
|
|
65
113
|
/**
|
|
66
114
|
* Valid disposition values for implementer disposition events.
|
|
67
115
|
*/
|
|
68
116
|
const VALID_DISPOSITIONS = Object.freeze([
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
'CHANGES_MADE',
|
|
118
|
+
'PUSHBACK_ALL',
|
|
119
|
+
'PARKED',
|
|
120
|
+
'BLOCKED'
|
|
73
121
|
]);
|
|
74
|
-
|
|
122
|
+
exports.VALID_DISPOSITIONS = VALID_DISPOSITIONS;
|
|
75
123
|
/**
|
|
76
124
|
* Valid review verdict values for reviewer outcome events.
|
|
77
125
|
*/
|
|
78
126
|
const VALID_VERDICTS = Object.freeze([
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
'approve',
|
|
128
|
+
'request-changes',
|
|
129
|
+
'comment'
|
|
82
130
|
]);
|
|
83
|
-
|
|
131
|
+
exports.VALID_VERDICTS = VALID_VERDICTS;
|
|
84
132
|
// -------- Path Resolution --------
|
|
85
|
-
|
|
86
133
|
/**
|
|
87
134
|
* Returns the absolute path to the mission-local review-events directory.
|
|
88
135
|
* Creates the directory if it does not exist.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} slug - Mission slug
|
|
91
|
-
* @param {string} [rootDir] - Root directory to resolve from (defaults to process.cwd())
|
|
92
|
-
* @returns {string|null} - Absolute path to review-events dir, or null if mission dir not found
|
|
93
136
|
*/
|
|
94
137
|
function reviewEventsDir(slug, rootDir = process.cwd()) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
138
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, rootDir);
|
|
139
|
+
if (!missionDir) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const dir = path.join(missionDir, 'review-events');
|
|
143
|
+
if (!fs.existsSync(dir)) {
|
|
144
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
return dir;
|
|
102
147
|
}
|
|
103
|
-
|
|
104
148
|
/**
|
|
105
149
|
* Returns the path for a specific event file in the mission-local store.
|
|
106
|
-
*
|
|
107
|
-
* Filename pattern: <timestamp>-<classification>-<round>-<actor>.md
|
|
108
|
-
* timestamp: ISO 8601 date-time without colons (e.g., 2026-05-25T143022)
|
|
109
|
-
* classification: event type from VALID_EVENT_TYPES
|
|
110
|
-
* round: review round number
|
|
111
|
-
* actor: agent family name or 'human'
|
|
112
|
-
*
|
|
113
|
-
* @param {string} slug - Mission slug
|
|
114
|
-
* @param {string} eventType - One of VALID_EVENT_TYPES
|
|
115
|
-
* @param {number} round - Review round number
|
|
116
|
-
* @param {string} actor - Agent family or 'human'
|
|
117
|
-
* @param {string} [timestamp] - Optional ISO timestamp; defaults to now
|
|
118
|
-
* @param {string} [rootDir] - Root directory to resolve from
|
|
119
|
-
* @returns {string|null} - Absolute file path, or null if mission dir not found
|
|
120
150
|
*/
|
|
121
|
-
function eventFilePath(slug, eventType, round, actor,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
function eventFilePath(slug, eventType, round, actor, timestamp = null, rootDir = process.cwd()) {
|
|
152
|
+
const eventsDir = reviewEventsDir(slug, rootDir);
|
|
153
|
+
if (!eventsDir) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const ts = timestamp || generateEventTimestamp();
|
|
157
|
+
const sanitizedActor = sanitizeFilename(actor);
|
|
158
|
+
const filename = `${ts}-${eventType}-${round}-${sanitizedActor}.md`;
|
|
159
|
+
return path.join(eventsDir, filename);
|
|
129
160
|
}
|
|
130
|
-
|
|
131
161
|
/**
|
|
132
162
|
* Returns the legacy /tmp/ artifact path for compatibility.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} slug - Mission slug
|
|
135
|
-
* @param {string} artifactName - e.g., 'review-findings.md'
|
|
136
|
-
* @param {string} [tmpDir] - Temp directory, defaults to os.tmpdir()
|
|
137
|
-
* @returns {string} - Full path to the /tmp/ artifact
|
|
138
163
|
*/
|
|
139
164
|
function legacyArtifactPath(slug, artifactName, tmpDir = os.tmpdir()) {
|
|
140
|
-
|
|
165
|
+
return path.join(tmpDir, `${slug}-${artifactName}`);
|
|
141
166
|
}
|
|
142
|
-
|
|
143
167
|
// -------- Timestamp & Sanitization --------
|
|
144
|
-
|
|
145
168
|
/**
|
|
146
169
|
* Generate a filesystem-safe ISO timestamp without colons.
|
|
147
|
-
* @returns {string} - e.g., '2026-05-25T143022'
|
|
148
170
|
*/
|
|
149
171
|
function generateEventTimestamp() {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return `${datePart}T${timePart.replace(/:/g, '')}`;
|
|
172
|
+
const now = new Date();
|
|
173
|
+
const datePart = now.toISOString().split('T')[0];
|
|
174
|
+
const timePart = now.toISOString().split('T')[1].split('.')[0];
|
|
175
|
+
return `${datePart}T${timePart.replace(/:/g, '')}`;
|
|
155
176
|
}
|
|
156
|
-
|
|
157
177
|
/**
|
|
158
178
|
* Sanitize a string for use in a filename.
|
|
159
|
-
* Removes all non-alphanumeric characters except hyphens and underscores.
|
|
160
|
-
* @param {string} str - Input string
|
|
161
|
-
* @returns {string} - Filesystem-safe string
|
|
162
179
|
*/
|
|
163
180
|
function sanitizeFilename(str) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
if (!str) {
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
return String(str)
|
|
185
|
+
.toLowerCase()
|
|
186
|
+
.replace(/[@.]/g, '')
|
|
187
|
+
.replace(/[^a-z0-9_-]/g, '-')
|
|
188
|
+
.replace(/-+/g, '-')
|
|
189
|
+
.replace(/^-|-$/g, '');
|
|
173
190
|
}
|
|
174
|
-
|
|
175
191
|
// -------- Event Validation --------
|
|
176
|
-
|
|
177
192
|
/**
|
|
178
193
|
* Validate an event type against the taxonomy.
|
|
179
|
-
* @param {string} eventType - The type to validate
|
|
180
|
-
* @returns {boolean} - True if valid
|
|
181
194
|
*/
|
|
182
|
-
function isValidEventType(
|
|
183
|
-
|
|
195
|
+
function isValidEventType(eventType) {
|
|
196
|
+
return ALL_EVENT_TYPES.includes(eventType);
|
|
184
197
|
}
|
|
185
|
-
|
|
186
198
|
/**
|
|
187
199
|
* Validate a disposition value.
|
|
188
|
-
* @param {string} disposition - The disposition to validate
|
|
189
|
-
* @returns {boolean} - True if valid
|
|
190
200
|
*/
|
|
191
201
|
function isValidDisposition(disposition) {
|
|
192
|
-
|
|
202
|
+
return VALID_DISPOSITIONS.includes(disposition);
|
|
193
203
|
}
|
|
194
|
-
|
|
195
204
|
/**
|
|
196
205
|
* Validate a verdict value.
|
|
197
|
-
* @param {string} verdict - The verdict to validate
|
|
198
|
-
* @returns {boolean} - True if valid
|
|
199
206
|
*/
|
|
200
207
|
function isValidVerdict(verdict) {
|
|
201
|
-
|
|
208
|
+
return VALID_VERDICTS.includes(verdict);
|
|
202
209
|
}
|
|
203
|
-
|
|
204
210
|
/**
|
|
205
211
|
* Check if an event type should be mirrored to the review provider.
|
|
206
|
-
* @param {string} eventType - The event type
|
|
207
|
-
* @returns {boolean} - True if mirrored
|
|
208
212
|
*/
|
|
209
|
-
function shouldMirrorToProvider(
|
|
210
|
-
|
|
213
|
+
function shouldMirrorToProvider(eventType) {
|
|
214
|
+
return MIRRORED_EVENT_TYPES.has(eventType);
|
|
211
215
|
}
|
|
212
|
-
|
|
213
|
-
// -------- Event Structure --------
|
|
214
|
-
|
|
215
216
|
/**
|
|
216
217
|
* Normalize an event payload into a structured format.
|
|
217
|
-
* Extracts metadata from content and ensures required fields are present.
|
|
218
|
-
*
|
|
219
|
-
* @param {string} content - The raw event content (markdown)
|
|
220
|
-
* @param {string} eventType - The event classification
|
|
221
|
-
* @param {Object} [metadata] - Additional metadata (round, phase, actor, etc.)
|
|
222
|
-
* @returns {Object} - Normalized event structure
|
|
223
|
-
*/
|
|
224
|
-
/**
|
|
225
|
-
* @param {string} content
|
|
226
|
-
* @param {string} eventType
|
|
227
|
-
* @param {{[key: string]: any}} [metadata]
|
|
228
|
-
* @returns {{eventType: string, timestamp: string, content: string}}
|
|
229
218
|
*/
|
|
230
219
|
function normalizeEventContent(content, eventType, metadata = {}) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return payload;
|
|
220
|
+
const payload = {
|
|
221
|
+
eventType,
|
|
222
|
+
timestamp: '',
|
|
223
|
+
content,
|
|
224
|
+
...metadata
|
|
225
|
+
};
|
|
226
|
+
payload.eventType = eventType;
|
|
227
|
+
payload.timestamp = payload.timestamp || new Date().toISOString();
|
|
228
|
+
payload.content = content;
|
|
229
|
+
return payload;
|
|
242
230
|
}
|
|
243
|
-
|
|
244
231
|
/**
|
|
245
232
|
* Build the frontmatter for an event file.
|
|
246
|
-
* Uses YAML-style frontmatter for structured metadata.
|
|
247
|
-
*
|
|
248
|
-
* @param {Object} event - Normalized event structure
|
|
249
|
-
* @returns {string} - Frontmatter block
|
|
250
|
-
*/
|
|
251
|
-
/**
|
|
252
|
-
* @param {{eventType: string, timestamp: string, round?: number, phase?: string, actor?: string, slug?: string, disposition?: string, verdict?: string, fixedItems?: string[], pushedBackItems?: string[], parkedItems?: string[], blockedReason?: string, followUpReference?: string}} event
|
|
253
|
-
* @returns {string}
|
|
254
233
|
*/
|
|
255
234
|
function buildEventFrontmatter(event) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
lines.
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
lines.push('---');
|
|
299
|
-
return lines.join('\n');
|
|
235
|
+
const lines = [
|
|
236
|
+
'---',
|
|
237
|
+
`event_type: ${event.eventType}`,
|
|
238
|
+
`timestamp: ${event.timestamp}`,
|
|
239
|
+
];
|
|
240
|
+
if (event.round !== undefined) {
|
|
241
|
+
lines.push(`round: ${event.round}`);
|
|
242
|
+
}
|
|
243
|
+
if (event.phase !== undefined) {
|
|
244
|
+
lines.push(`phase: ${event.phase}`);
|
|
245
|
+
}
|
|
246
|
+
if (event.actor !== undefined) {
|
|
247
|
+
lines.push(`actor: ${event.actor}`);
|
|
248
|
+
}
|
|
249
|
+
if (event.slug !== undefined) {
|
|
250
|
+
lines.push(`slug: ${event.slug}`);
|
|
251
|
+
}
|
|
252
|
+
if (event.disposition !== undefined) {
|
|
253
|
+
lines.push(`disposition: ${event.disposition}`);
|
|
254
|
+
}
|
|
255
|
+
if (event.verdict !== undefined) {
|
|
256
|
+
lines.push(`verdict: ${event.verdict}`);
|
|
257
|
+
}
|
|
258
|
+
if (event.fixedItems !== undefined) {
|
|
259
|
+
lines.push(`fixed_items: ${JSON.stringify(event.fixedItems)}`);
|
|
260
|
+
}
|
|
261
|
+
if (event.pushedBackItems !== undefined) {
|
|
262
|
+
lines.push(`pushed_back_items: ${JSON.stringify(event.pushedBackItems)}`);
|
|
263
|
+
}
|
|
264
|
+
if (event.parkedItems !== undefined) {
|
|
265
|
+
lines.push(`parked_items: ${JSON.stringify(event.parkedItems)}`);
|
|
266
|
+
}
|
|
267
|
+
if (event.blockedReason !== undefined) {
|
|
268
|
+
lines.push(`blocked_reason: "${event.blockedReason.replace(/"/g, '\\"')}"`);
|
|
269
|
+
}
|
|
270
|
+
if (event.followUpReference !== undefined) {
|
|
271
|
+
lines.push(`followup_reference: ${event.followUpReference}`);
|
|
272
|
+
}
|
|
273
|
+
lines.push('---');
|
|
274
|
+
return lines.join('\n');
|
|
300
275
|
}
|
|
301
|
-
|
|
302
276
|
/**
|
|
303
277
|
* Build the workflow metadata footer for an event.
|
|
304
|
-
* Matches the existing pattern in review.js:793-797
|
|
305
|
-
*
|
|
306
|
-
* @param {string} slug - Mission slug
|
|
307
|
-
* @param {number} round - Review round
|
|
308
|
-
* @param {string} phase - Current phase
|
|
309
|
-
* @returns {string} - Metadata footer
|
|
310
278
|
*/
|
|
311
279
|
function buildEventFooter(slug, round, phase) {
|
|
312
|
-
|
|
280
|
+
return `\n\n---\n\`[workflow-round:${round}, workflow-phase:${phase}]\``;
|
|
313
281
|
}
|
|
314
|
-
|
|
315
282
|
/**
|
|
316
283
|
* Check if a comment body contains the workflow metadata footer.
|
|
317
|
-
* Comments with this footer are workflow-generated; comments without are human.
|
|
318
|
-
*
|
|
319
|
-
* @param {string} body - Comment body text
|
|
320
|
-
* @returns {boolean} - true if comment has workflow footer
|
|
321
284
|
*/
|
|
322
285
|
function hasWorkflowFooter(body) {
|
|
323
|
-
|
|
286
|
+
return /\`\[workflow-round:\d+, workflow-phase:[^\]]+\]\`/.test(body);
|
|
324
287
|
}
|
|
325
|
-
|
|
326
288
|
/**
|
|
327
289
|
* Classify a comment as either workflow-generated or human.
|
|
328
|
-
* Workflow-generated comments have the metadata footer; human comments do not.
|
|
329
|
-
*
|
|
330
|
-
* @param {Object} comment - Comment object with body field
|
|
331
|
-
* @returns {string|null} - 'human_note' if human, null if workflow-generated
|
|
332
290
|
*/
|
|
333
|
-
function classifyComment(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Human comments (no workflow footer) are classified as human_note
|
|
340
|
-
return VALID_EVENT_TYPES.HUMAN_NOTE;
|
|
291
|
+
function classifyComment(comment) {
|
|
292
|
+
const body = comment.body || '';
|
|
293
|
+
if (hasWorkflowFooter(body)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return VALID_EVENT_TYPES.HUMAN_NOTE;
|
|
341
297
|
}
|
|
342
|
-
|
|
343
298
|
/**
|
|
344
299
|
* Consume human notes from provider PR comments.
|
|
345
|
-
* Reads PR comments, classifies human (non-workflow) comments, and creates
|
|
346
|
-
* human_note events for them. Human comments do not have the workflow metadata footer.
|
|
347
|
-
*
|
|
348
|
-
* This function satisfies SC 6: Human provider comments are classified as human_note
|
|
349
|
-
* or ignored as non-round-affecting input by an explicit workflow rule; neither path
|
|
350
|
-
* can mutate review-state.json without a workflow command that records a repo-owned event.
|
|
351
|
-
*
|
|
352
|
-
* @param {string} slug - Mission slug
|
|
353
|
-
* @param {string} actor - Actor family (e.g., 'codex', 'mistral') or 'human'
|
|
354
|
-
* @param {Object} [options] - Options
|
|
355
|
-
* @param {Function} [options.getCommentsFn] - Function to get provider comments
|
|
356
|
-
* @param {Function} [options.createEventFn] - Function to create events (default: createEvent)
|
|
357
|
-
* @param {Function} [options.readTokenFn] - Function to read provider token
|
|
358
|
-
* @param {string} [options.worktree] - Working tree directory
|
|
359
|
-
* @param {Function} [options.log] - Log function
|
|
360
|
-
* @param {Function} [options.error] - Error function
|
|
361
|
-
* @returns {Object} - { ok: boolean, created: Array, skipped: Array }
|
|
362
|
-
*/
|
|
363
|
-
/**
|
|
364
|
-
* @param {string} slug
|
|
365
|
-
* @param {string} actor
|
|
366
|
-
* @param {{getCommentsFn?: Function, createEventFn?: Function, readTokenFn?: Function, reviewIdentity?: string|null, forgejoUser?: string|null, worktree?: string, log?: Function, error?: Function}} [options]
|
|
367
|
-
* @returns {Promise<{ok: boolean, created: Array<any>, skipped: Array<any>, error?: string}>}
|
|
368
300
|
*/
|
|
369
301
|
async function consumeHumanNotes(slug, actor, options = {}) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
forgejoUser = null,
|
|
376
|
-
worktree,
|
|
377
|
-
log = fmt.log.plain,
|
|
378
|
-
error = fmt.log.plainError
|
|
379
|
-
} = options;
|
|
380
|
-
|
|
381
|
-
const actorIdentity = reviewIdentity || forgejoUser;
|
|
382
|
-
if (!actorIdentity) {
|
|
383
|
-
error(fmt.status('FAIL', 'Review identity is required for consumeHumanNotes.'));
|
|
384
|
-
return { ok: false, created: [], skipped: [], error: 'reviewIdentity required' };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// In sandbox/Codex environments, the provider may be unreachable from Node subprocesses.
|
|
388
|
-
// This is acceptable - human note classification is best-effort
|
|
389
|
-
if (!getCommentsFn) {
|
|
390
|
-
log(fmt.status('INFO', 'consumeHumanNotes: no getCommentsFn provided; skipping human note classification.'));
|
|
391
|
-
return { ok: true, created: [], skipped: [] };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const token = readTokenFn ? readTokenFn(actorIdentity) : null;
|
|
395
|
-
if (!token) {
|
|
396
|
-
log(fmt.status('INFO', 'consumeHumanNotes: no provider token found; skipping human note classification.'));
|
|
397
|
-
return { ok: true, created: [], skipped: [] };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const branch = missionBranchName(slug, worktree || process.cwd());
|
|
401
|
-
const comments = await getCommentsFn(branch, token);
|
|
402
|
-
|
|
403
|
-
if (!comments || !Array.isArray(comments)) {
|
|
404
|
-
log(fmt.status('INFO', 'consumeHumanNotes: no comments retrieved; skipping classification.'));
|
|
405
|
-
return { ok: true, created: [], skipped: [] };
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const created = [];
|
|
409
|
-
const skipped = [];
|
|
410
|
-
|
|
411
|
-
// Get current review state for round/phase metadata
|
|
412
|
-
const currentState = readReviewState(slug, worktree || process.cwd());
|
|
413
|
-
const round = currentState ? currentState.round : 1;
|
|
414
|
-
const phase = currentState ? currentState.phase : 'reviewing';
|
|
415
|
-
|
|
416
|
-
for (const comment of comments) {
|
|
417
|
-
// Skip workflow-generated comments (have the metadata footer)
|
|
418
|
-
if (hasWorkflowFooter(comment.body || '')) {
|
|
419
|
-
skipped.push({ user: comment.user, created: comment.created, reason: 'workflow-generated' });
|
|
420
|
-
continue;
|
|
302
|
+
const { getCommentsFn, createEventFn = createEvent, readTokenFn, reviewIdentity = null, forgejoUser = null, worktree, log: logger = fmt.log.plain, error = fmt.log.plainError } = options;
|
|
303
|
+
const actorIdentity = reviewIdentity || forgejoUser;
|
|
304
|
+
if (!actorIdentity) {
|
|
305
|
+
error(fmt.status('FAIL', 'Review identity is required for consumeHumanNotes.'));
|
|
306
|
+
return { ok: false, created: [], skipped: [], error: 'reviewIdentity required' };
|
|
421
307
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (!classification) {
|
|
426
|
-
skipped.push({ user: comment.user, created: comment.created, reason: 'already-classified' });
|
|
427
|
-
continue;
|
|
308
|
+
if (!getCommentsFn) {
|
|
309
|
+
logger(fmt.status('INFO', 'consumeHumanNotes: no getCommentsFn provided; skipping human note classification.'));
|
|
310
|
+
return { ok: true, created: [], skipped: [] };
|
|
428
311
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
round,
|
|
434
|
-
phase,
|
|
435
|
-
actor: actor || comment.user || 'human'
|
|
436
|
-
}, {
|
|
437
|
-
worktree: worktree || process.cwd(),
|
|
438
|
-
skipGit: true,
|
|
439
|
-
log,
|
|
440
|
-
error
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
if (result.ok) {
|
|
444
|
-
created.push({ path: result.path, user: comment.user, created: comment.created });
|
|
445
|
-
} else {
|
|
446
|
-
error(fmt.status('WARN', `Failed to create human_note event for comment by ${comment.user}: ${result.error}`));
|
|
312
|
+
const token = readTokenFn ? readTokenFn(actorIdentity) : null;
|
|
313
|
+
if (!token) {
|
|
314
|
+
logger(fmt.status('INFO', 'consumeHumanNotes: no provider token found; skipping human note classification.'));
|
|
315
|
+
return { ok: true, created: [], skipped: [] };
|
|
447
316
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
317
|
+
const branch = (0, mission_utils_js_1.missionBranchName)(slug, worktree || process.cwd());
|
|
318
|
+
const comments = await getCommentsFn(branch, token);
|
|
319
|
+
if (!comments || !Array.isArray(comments)) {
|
|
320
|
+
logger(fmt.status('INFO', 'consumeHumanNotes: no comments retrieved; skipping classification.'));
|
|
321
|
+
return { ok: true, created: [], skipped: [] };
|
|
322
|
+
}
|
|
323
|
+
const created = [];
|
|
324
|
+
const skipped = [];
|
|
325
|
+
const currentState = (0, review_state_js_1.readReviewState)(slug, worktree || process.cwd());
|
|
326
|
+
const round = currentState ? currentState.round : 1;
|
|
327
|
+
const phase = currentState ? currentState.phase : 'reviewing';
|
|
328
|
+
for (const comment of comments) {
|
|
329
|
+
if (hasWorkflowFooter(comment.body || '')) {
|
|
330
|
+
skipped.push({ user: comment.user, created: comment.created, reason: 'workflow-generated' });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const classification = classifyComment(comment);
|
|
334
|
+
if (!classification) {
|
|
335
|
+
skipped.push({ user: comment.user, created: comment.created, reason: 'already-classified' });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const result = createEventFn(slug, classification, {
|
|
339
|
+
content: comment.body || '',
|
|
340
|
+
round,
|
|
341
|
+
phase,
|
|
342
|
+
actor: actor || comment.user || 'human'
|
|
343
|
+
}, {
|
|
344
|
+
worktree: worktree || process.cwd(),
|
|
345
|
+
skipGit: true,
|
|
346
|
+
log: logger,
|
|
347
|
+
error
|
|
348
|
+
});
|
|
349
|
+
if (result.ok) {
|
|
350
|
+
created.push({ path: result.path, user: comment.user, created: comment.created });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
error(fmt.status('WARN', `Failed to create human_note event for comment by ${comment.user}: ${result.error}`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
logger(fmt.status('INFO', `consumeHumanNotes: created ${created.length} human_note events, skipped ${skipped.length} workflow comments`));
|
|
357
|
+
return { ok: true, created, skipped };
|
|
452
358
|
}
|
|
453
|
-
|
|
454
359
|
/**
|
|
455
360
|
* Render a complete event file content.
|
|
456
|
-
*
|
|
457
|
-
* @param {Object} event - Normalized event structure
|
|
458
|
-
* @returns {string} - Complete file content
|
|
459
|
-
*/
|
|
460
|
-
/**
|
|
461
|
-
* @param {{content?: string, round?: number, phase?: string, slug?: string, eventType?: string, timestamp?: string}} event
|
|
462
|
-
* @returns {string}
|
|
463
361
|
*/
|
|
464
362
|
function renderEventFile(event) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// Add footer if we have round and phase
|
|
476
|
-
const footer = event.round !== undefined && event.phase !== undefined
|
|
477
|
-
? buildEventFooter(/** @type {string} */ (event.slug), event.round, event.phase)
|
|
478
|
-
: '';
|
|
479
|
-
|
|
480
|
-
return `${frontmatter}\n\n${content}${footer}`;
|
|
363
|
+
const frontmatter = buildEventFrontmatter(event);
|
|
364
|
+
const content = event.content || '';
|
|
365
|
+
const hasFooter = /\n\n---\n`\[workflow-round:\d+, workflow-phase:[^\]]+\]`/.test(content);
|
|
366
|
+
if (hasFooter) {
|
|
367
|
+
return `${frontmatter}\n\n${content}`;
|
|
368
|
+
}
|
|
369
|
+
const footer = event.round !== undefined && event.phase !== undefined
|
|
370
|
+
? buildEventFooter(event.slug || '', event.round, event.phase)
|
|
371
|
+
: '';
|
|
372
|
+
return `${frontmatter}\n\n${content}${footer}`;
|
|
481
373
|
}
|
|
482
|
-
|
|
483
374
|
// -------- Event Creation --------
|
|
484
|
-
|
|
375
|
+
/** @typedef {{content: string, round?: number, phase?: string, actor?: string, disposition?: string, verdict?: string, fixedItems?: unknown[], pushedBackItems?: unknown[], parkedItems?: unknown[], blockedReason?: string, followUpReference?: string, timestamp?: string}} CreateEventParams */
|
|
485
376
|
/**
|
|
486
377
|
* Create and persist a classified review event.
|
|
487
|
-
*
|
|
488
|
-
* @param {string} slug - Mission slug
|
|
489
|
-
* @param {string} eventType - One of VALID_EVENT_TYPES
|
|
490
|
-
* @param {Object} params - Event parameters
|
|
491
|
-
* @param {string} params.content - The event content (markdown)
|
|
492
|
-
* @param {number} [params.round] - Review round number (from state)
|
|
493
|
-
* @param {string} [params.phase] - Current phase (from state)
|
|
494
|
-
* @param {string} [params.actor] - Agent family or 'human'
|
|
495
|
-
* @param {string} [params.disposition] - For implementer disposition events
|
|
496
|
-
* @param {string} [params.verdict] - For reviewer outcome events
|
|
497
|
-
* @param {Array<any>} [params.fixedItems] - For implementer round summary
|
|
498
|
-
* @param {Array<any>} [params.pushedBackItems] - For implementer round summary
|
|
499
|
-
* @param {Array<any>} [params.parkedItems] - For implementer round summary
|
|
500
|
-
* @param {string} [params.blockedReason] - For BLOCKED events
|
|
501
|
-
* @param {string} [params.followUpReference] - For PARKED_FOLLOWUP events
|
|
502
|
-
* @param {string} [params.timestamp] - Custom timestamp
|
|
503
|
-
* @param {string} [rootDir] - Root directory
|
|
504
|
-
* @param {Object} [options] - Additional options
|
|
505
|
-
* @param {boolean} [options.skipGit] - Skip git add/commit
|
|
506
|
-
* @param {Function} [options.gitFn] - Git function override for testing
|
|
507
|
-
* @returns {Object} - { ok: boolean, path: string|null, error: string|null }
|
|
508
|
-
*/
|
|
509
|
-
/**
|
|
510
|
-
* @param {string} slug
|
|
511
|
-
* @param {string} eventType
|
|
512
|
-
* @param {{content: string, round?: number, phase?: string, actor?: string, disposition?: string, verdict?: string, fixedItems?: Array<any>, pushedBackItems?: Array<any>, parkedItems?: Array<any>, blockedReason?: string, followUpReference?: string, timestamp?: string}} params
|
|
513
|
-
* @param {{skipGit?: boolean, gitFn?: Function, log?: Function, error?: Function, worktree?: string, allowMissingRequiredFields?: boolean}} [options]
|
|
514
|
-
* @returns {{ok: boolean, path: string|null, error?: string|null, event?: Object}}
|
|
515
378
|
*/
|
|
516
379
|
function createEvent(slug, eventType, params, options = {}) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const rootDir = worktree || resolveWorktree(slug) || process.cwd();
|
|
527
|
-
|
|
528
|
-
// Validate event type
|
|
529
|
-
if (!isValidEventType(eventType)) {
|
|
530
|
-
error(fmt.status('FAIL', `Invalid event type "${eventType}". Valid types: ${ALL_EVENT_TYPES.join(', ')}`));
|
|
531
|
-
return { ok: false, path: null, error: `Invalid event type: ${eventType}` };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Validate disposition if provided
|
|
535
|
-
if (params.disposition !== undefined && !isValidDisposition(params.disposition)) {
|
|
536
|
-
error(fmt.status('FAIL', `Invalid disposition "${params.disposition}". Valid: ${VALID_DISPOSITIONS.join(', ')}`));
|
|
537
|
-
return { ok: false, path: null, error: `Invalid disposition: ${params.disposition}` };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Validate verdict if provided
|
|
541
|
-
if (params.verdict !== undefined && !isValidVerdict(params.verdict)) {
|
|
542
|
-
error(fmt.status('FAIL', `Invalid verdict "${params.verdict}". Valid: ${VALID_VERDICTS.join(', ')}`));
|
|
543
|
-
return { ok: false, path: null, error: `Invalid verdict: ${params.verdict}` };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Validate required fields for specific event types
|
|
547
|
-
// Skip validation for legacy imports which may not have all required fields
|
|
548
|
-
if (!allowMissingRequiredFields) {
|
|
549
|
-
if (eventType === VALID_EVENT_TYPES.REVIEWER_OUTCOME && !params.verdict) {
|
|
550
|
-
error(fmt.status('FAIL', `reviewer_outcome event requires --verdict`));
|
|
551
|
-
return { ok: false, path: null, error: 'reviewer_outcome event requires verdict' };
|
|
380
|
+
const { skipGit = false, gitFn = git_js_1.git, log: logger = fmt.log.plain, error = fmt.log.plainError, worktree, allowMissingRequiredFields = false } = options;
|
|
381
|
+
const rootDir = worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
|
|
382
|
+
if (!isValidEventType(eventType)) {
|
|
383
|
+
error(fmt.status('FAIL', `Invalid event type "${eventType}". Valid types: ${ALL_EVENT_TYPES.join(', ')}`));
|
|
384
|
+
return { ok: false, path: null, error: `Invalid event type: ${eventType}` };
|
|
385
|
+
}
|
|
386
|
+
if (params.disposition !== undefined && !isValidDisposition(params.disposition)) {
|
|
387
|
+
error(fmt.status('FAIL', `Invalid disposition "${params.disposition}". Valid: ${VALID_DISPOSITIONS.join(', ')}`));
|
|
388
|
+
return { ok: false, path: null, error: `Invalid disposition: ${params.disposition}` };
|
|
552
389
|
}
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
390
|
+
if (params.verdict !== undefined && !isValidVerdict(params.verdict)) {
|
|
391
|
+
error(fmt.status('FAIL', `Invalid verdict "${params.verdict}". Valid: ${VALID_VERDICTS.join(', ')}`));
|
|
392
|
+
return { ok: false, path: null, error: `Invalid verdict: ${params.verdict}` };
|
|
556
393
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Render and write
|
|
600
|
-
const content = renderEventFile(event);
|
|
601
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
602
|
-
|
|
603
|
-
log(fmt.status('PASS', `Created review event: ${path.basename(filePath)}`));
|
|
604
|
-
|
|
605
|
-
// Git add and commit
|
|
606
|
-
if (!skipGit) {
|
|
607
|
-
const relPath = path.relative(rootDir, filePath);
|
|
608
|
-
const result = gitFn(['-C', rootDir, 'add', relPath]);
|
|
609
|
-
if (result.status !== 0) {
|
|
610
|
-
error(fmt.status('WARN', `Failed to git add event file: ${result.stderr}`));
|
|
611
|
-
} else {
|
|
612
|
-
const commitMsg = `review-event(${slug}): ${eventType} round ${round} (${phase}) [${actor}]`;
|
|
613
|
-
const commitResult = gitFn(['-C', rootDir, 'commit', '-m', commitMsg, '--allow-empty']);
|
|
614
|
-
if (commitResult.status !== 0) {
|
|
615
|
-
error(fmt.status('WARN', `Failed to commit event file: ${commitResult.stderr}`));
|
|
616
|
-
} else {
|
|
617
|
-
log(fmt.status('PASS', `Committed review event to git`));
|
|
618
|
-
}
|
|
394
|
+
if (!allowMissingRequiredFields) {
|
|
395
|
+
if (eventType === VALID_EVENT_TYPES.REVIEWER_OUTCOME && !params.verdict) {
|
|
396
|
+
error(fmt.status('FAIL', 'reviewer_outcome event requires --verdict'));
|
|
397
|
+
return { ok: false, path: null, error: 'reviewer_outcome event requires verdict' };
|
|
398
|
+
}
|
|
399
|
+
if (eventType === VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION && !params.disposition) {
|
|
400
|
+
error(fmt.status('FAIL', 'implementer_disposition event requires --disposition'));
|
|
401
|
+
return { ok: false, path: null, error: 'implementer_disposition event requires disposition' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
let state;
|
|
405
|
+
if (params.round === undefined || params.phase === undefined) {
|
|
406
|
+
state = (0, review_state_js_1.readReviewState)(slug, rootDir);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
state = null;
|
|
410
|
+
}
|
|
411
|
+
const round = (params.round !== undefined ? params.round : (state ? state.round : 1));
|
|
412
|
+
const phase = (params.phase !== undefined ? params.phase : (state ? state.phase : 'reviewing'));
|
|
413
|
+
const actor = params.actor || 'unknown';
|
|
414
|
+
const event = normalizeEventContent(params.content || '', eventType, {
|
|
415
|
+
slug,
|
|
416
|
+
round,
|
|
417
|
+
phase,
|
|
418
|
+
actor,
|
|
419
|
+
eventType,
|
|
420
|
+
timestamp: params.timestamp,
|
|
421
|
+
disposition: params.disposition,
|
|
422
|
+
verdict: params.verdict,
|
|
423
|
+
fixedItems: params.fixedItems,
|
|
424
|
+
pushedBackItems: params.pushedBackItems,
|
|
425
|
+
parkedItems: params.parkedItems,
|
|
426
|
+
blockedReason: params.blockedReason,
|
|
427
|
+
followUpReference: params.followUpReference
|
|
428
|
+
});
|
|
429
|
+
const ts = params.timestamp ?? null;
|
|
430
|
+
const filePath = eventFilePath(slug, eventType, round, actor, ts, rootDir);
|
|
431
|
+
if (!filePath) {
|
|
432
|
+
error(fmt.status('FAIL', `Cannot resolve mission directory for slug "${slug}"`));
|
|
433
|
+
return { ok: false, path: null, error: `Mission directory not found` };
|
|
619
434
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
435
|
+
const dir = path.dirname(filePath);
|
|
436
|
+
if (!fs.existsSync(dir)) {
|
|
437
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
438
|
+
}
|
|
439
|
+
const content = renderEventFile(event);
|
|
440
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
441
|
+
logger(fmt.status('PASS', `Created review event: ${path.basename(filePath)}`));
|
|
442
|
+
if (!skipGit) {
|
|
443
|
+
const relPath = path.relative(rootDir, filePath);
|
|
444
|
+
const result = gitFn(['-C', rootDir, 'add', relPath]);
|
|
445
|
+
if (result.status !== 0) {
|
|
446
|
+
error(fmt.status('WARN', `Failed to git add event file: ${result.stderr}`));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
const commitMsg = `review-event(${slug}): ${eventType} round ${round} (${phase}) [${actor}]`;
|
|
450
|
+
const commitResult = gitFn(['-C', rootDir, 'commit', '-m', commitMsg, '--allow-empty']);
|
|
451
|
+
if (commitResult.status !== 0) {
|
|
452
|
+
error(fmt.status('WARN', `Failed to commit event file: ${commitResult.stderr}`));
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
logger(fmt.status('PASS', 'Committed review event to git'));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { ok: true, path: filePath, event };
|
|
623
460
|
}
|
|
624
|
-
|
|
625
461
|
/**
|
|
626
462
|
* Import an event from a legacy /tmp/ artifact file.
|
|
627
|
-
* Normalizes the content and stores it in the repo-owned mission store.
|
|
628
|
-
*
|
|
629
|
-
* @param {string} slug - Mission slug
|
|
630
|
-
* @param {string} artifactName - e.g., 'review-findings.md', 'round-resolution.md'
|
|
631
|
-
* @param {string} eventType - The classification to assign
|
|
632
|
-
* @param {Object} [params] - Additional parameters (round, phase, actor, etc.)
|
|
633
|
-
* @param {Object} [options] - Options (same as createEvent)
|
|
634
|
-
* @returns {Object} - { ok: boolean, path: string|null, importedFrom: string|null, error: string|null }
|
|
635
|
-
*/
|
|
636
|
-
/**
|
|
637
|
-
* @param {string} slug
|
|
638
|
-
* @param {string} artifactName
|
|
639
|
-
* @param {string} eventType
|
|
640
|
-
* @param {{round?: number, phase?: string, actor?: string}} [params]
|
|
641
|
-
* @param {{tmpDir?: string, log?: Function, error?: Function, worktree?: string}} [options]
|
|
642
|
-
* @returns {{ok: boolean, path: string|null, importedFrom: string|null, event?: Object, skipped?: boolean, legacyPath?: string, error?: string}}
|
|
643
463
|
*/
|
|
644
464
|
function importLegacyArtifact(slug, artifactName, eventType, params = {}, options = {}) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
actor: params.actor || (/** @type {Function} */ (inferActorFromArtifact))()
|
|
676
|
-
}, { ...options, worktree: worktree || undefined, allowMissingRequiredFields: true });
|
|
677
|
-
|
|
678
|
-
if (!result.ok) {
|
|
679
|
-
return { ok: false, path: null, importedFrom: legacyPath, error: /** @type {string | undefined} */ (result.error) };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
log(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(/** @type {string} */ (result.path))}`));
|
|
683
|
-
|
|
684
|
-
return {
|
|
685
|
-
ok: true,
|
|
686
|
-
path: result.path,
|
|
687
|
-
importedFrom: legacyPath,
|
|
688
|
-
event: result.event,
|
|
689
|
-
legacyPath
|
|
690
|
-
};
|
|
465
|
+
const { tmpDir = os.tmpdir(), log: logger = fmt.log.plain, error = fmt.log.plainError, worktree } = options;
|
|
466
|
+
const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
|
|
467
|
+
if (!fs.existsSync(legacyPath)) {
|
|
468
|
+
logger(fmt.status('INFO', `Legacy artifact not found: ${legacyPath}`));
|
|
469
|
+
return { ok: true, path: null, importedFrom: legacyPath, skipped: true };
|
|
470
|
+
}
|
|
471
|
+
let content;
|
|
472
|
+
try {
|
|
473
|
+
content = fs.readFileSync(legacyPath, 'utf8');
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
error(fmt.status('FAIL', `Failed to read legacy artifact: ${err.message}`));
|
|
477
|
+
return { ok: false, path: null, importedFrom: legacyPath, error: err.message };
|
|
478
|
+
}
|
|
479
|
+
const result = createEvent(slug, eventType, {
|
|
480
|
+
...params,
|
|
481
|
+
content,
|
|
482
|
+
actor: params.actor || inferActorFromArtifact()
|
|
483
|
+
}, { ...options, worktree: worktree || undefined, allowMissingRequiredFields: true });
|
|
484
|
+
if (!result.ok) {
|
|
485
|
+
return { ok: false, path: null, importedFrom: legacyPath, error: result.error ?? undefined };
|
|
486
|
+
}
|
|
487
|
+
logger(fmt.status('PASS', `Imported legacy artifact ${artifactName} to ${path.basename(result.path || '')}`));
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
path: result.path,
|
|
491
|
+
importedFrom: legacyPath,
|
|
492
|
+
event: result.event,
|
|
493
|
+
legacyPath
|
|
494
|
+
};
|
|
691
495
|
}
|
|
692
|
-
|
|
693
496
|
/**
|
|
694
497
|
* Infer actor from legacy artifact name (best guess).
|
|
695
|
-
* In practice, the actor should be passed explicitly from the workflow context.
|
|
696
498
|
*/
|
|
697
499
|
function inferActorFromArtifact() {
|
|
698
|
-
|
|
699
|
-
return 'unknown';
|
|
500
|
+
return 'unknown';
|
|
700
501
|
}
|
|
701
|
-
|
|
702
502
|
/**
|
|
703
503
|
* Map legacy artifact names to event types.
|
|
704
504
|
*/
|
|
705
505
|
const LEGACY_ARTIFACT_TO_EVENT_TYPE = Object.freeze({
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
506
|
+
'review-findings.md': VALID_EVENT_TYPES.REVIEWER_FINDINGS,
|
|
507
|
+
'review-outcome.md': VALID_EVENT_TYPES.REVIEWER_OUTCOME,
|
|
508
|
+
'review-verdict.txt': VALID_EVENT_TYPES.REVIEWER_OUTCOME,
|
|
509
|
+
'round-resolution.md': VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY,
|
|
510
|
+
'review-disposition.txt': VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION,
|
|
711
511
|
});
|
|
712
|
-
|
|
512
|
+
exports.LEGACY_ARTIFACT_TO_EVENT_TYPE = LEGACY_ARTIFACT_TO_EVENT_TYPE;
|
|
713
513
|
/**
|
|
714
514
|
* Import all legacy /tmp/ artifacts for a mission.
|
|
715
|
-
* Used during the transition to repo-owned storage.
|
|
716
|
-
*
|
|
717
|
-
* Special handling: review-verdict.txt is treated as metadata (verdict field)
|
|
718
|
-
* for review-outcome.md, not as a standalone reviewer_outcome event.
|
|
719
|
-
* This prevents duplicate reviewer_outcome events.
|
|
720
|
-
*
|
|
721
|
-
* @param {string} slug - Mission slug
|
|
722
|
-
* @param {Object} [options] - Options
|
|
723
|
-
* @returns {Object} - { ok: boolean, imported: Array, errors: Array }
|
|
724
|
-
*/
|
|
725
|
-
/**
|
|
726
|
-
* @param {string} slug
|
|
727
|
-
* @param {{tmpDir?: string, error?: Function, worktree?: string, log?: Function}} [options]
|
|
728
|
-
* @returns {{ok: boolean, imported: Array<any>, errors: Array<any>}}
|
|
729
515
|
*/
|
|
730
516
|
function importAllLegacyArtifacts(slug, options = {}) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const state = readReviewState(slug, rootDir);
|
|
743
|
-
const round = state ? state.round : 1;
|
|
744
|
-
const phase = state ? state.phase : 'reviewing';
|
|
745
|
-
|
|
746
|
-
// Track which legacy files exist
|
|
747
|
-
/** @type {{[key: string]: string|null}} */
|
|
748
|
-
const legacyFiles = {};
|
|
749
|
-
for (const artifactName of Object.keys(LEGACY_ARTIFACT_TO_EVENT_TYPE)) {
|
|
750
|
-
const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
|
|
751
|
-
legacyFiles[artifactName] = fs.existsSync(legacyPath) ? legacyPath : null;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Special case: handle review-outcome.md and review-verdict.txt together
|
|
755
|
-
// review-verdict.txt contains the verdict metadata for review-outcome.md
|
|
756
|
-
const outcomePath = legacyFiles['review-outcome.md'];
|
|
757
|
-
const verdictPath = legacyFiles['review-verdict.txt'];
|
|
758
|
-
|
|
759
|
-
if (outcomePath && fs.existsSync(outcomePath)) {
|
|
760
|
-
// Read verdict content if it exists
|
|
761
|
-
let verdictContent = null;
|
|
762
|
-
if (verdictPath) {
|
|
763
|
-
try {
|
|
764
|
-
if (fs.existsSync(verdictPath)) {
|
|
765
|
-
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
766
|
-
}
|
|
767
|
-
} catch (/** @type {unknown} */ err) {
|
|
768
|
-
error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Import review-outcome.md with verdict as metadata (only include verdict if valid)
|
|
773
|
-
/** @type {{round: number, phase: string, actor: string, verdict?: string}} */
|
|
774
|
-
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
775
|
-
if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
|
|
776
|
-
outcomeParams.verdict = verdictContent;
|
|
517
|
+
const { tmpDir = os.tmpdir(), error = fmt.log.plainError, worktree } = options;
|
|
518
|
+
const rootDir = worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
|
|
519
|
+
const imported = [];
|
|
520
|
+
const errors = [];
|
|
521
|
+
const state = (0, review_state_js_1.readReviewState)(slug, rootDir);
|
|
522
|
+
const round = state ? state.round : 1;
|
|
523
|
+
const phase = state ? state.phase : 'reviewing';
|
|
524
|
+
const legacyFiles = {};
|
|
525
|
+
for (const artifactName of Object.keys(LEGACY_ARTIFACT_TO_EVENT_TYPE)) {
|
|
526
|
+
const legacyPath = legacyArtifactPath(slug, artifactName, tmpDir);
|
|
527
|
+
legacyFiles[artifactName] = fs.existsSync(legacyPath) ? legacyPath : null;
|
|
777
528
|
}
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
529
|
+
const outcomePath = legacyFiles['review-outcome.md'];
|
|
530
|
+
const verdictPath = legacyFiles['review-verdict.txt'];
|
|
531
|
+
if (outcomePath && fs.existsSync(outcomePath)) {
|
|
532
|
+
let verdictContent = null;
|
|
533
|
+
if (verdictPath) {
|
|
534
|
+
try {
|
|
535
|
+
if (fs.existsSync(verdictPath)) {
|
|
536
|
+
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
|
|
541
|
+
}
|
|
789
542
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
543
|
+
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
544
|
+
if (verdictContent && isValidVerdict(verdictContent)) {
|
|
545
|
+
outcomeParams.verdict = verdictContent;
|
|
546
|
+
}
|
|
547
|
+
const result = importLegacyArtifact(slug, 'review-outcome.md', VALID_EVENT_TYPES.REVIEWER_OUTCOME, outcomeParams, { ...options, worktree: rootDir });
|
|
548
|
+
if (result.ok) {
|
|
549
|
+
if (result.path) {
|
|
550
|
+
imported.push({ artifactName: 'review-outcome.md', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, path: result.path, legacyPath: result.legacyPath });
|
|
551
|
+
if (verdictPath && fs.existsSync(verdictPath)) {
|
|
552
|
+
imported.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, path: result.path, legacyPath: verdictPath, asMetadata: true });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
errors.push({ artifactName: 'review-outcome.md', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, error: result.error });
|
|
558
|
+
if (verdictPath && fs.existsSync(verdictPath)) {
|
|
559
|
+
const verdictResult = importLegacyArtifact(slug, 'review-verdict.txt', VALID_EVENT_TYPES.REVIEWER_OUTCOME, { round, phase, actor: 'legacy' }, { ...options, worktree: rootDir });
|
|
560
|
+
if (verdictResult.ok && verdictResult.path) {
|
|
561
|
+
imported.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, path: verdictResult.path, legacyPath: verdictResult.legacyPath });
|
|
562
|
+
}
|
|
563
|
+
else if (!verdictResult.ok) {
|
|
564
|
+
errors.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, error: verdictResult.error });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
802
567
|
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
} else if (verdictPath && fs.existsSync(verdictPath)) {
|
|
806
|
-
// Only review-verdict.txt exists (no review-outcome.md) - import it as a standalone event
|
|
807
|
-
// This is a fallback for incomplete artifact sets
|
|
808
|
-
// Read the verdict content from the file to use as the verdict param
|
|
809
|
-
let verdictContent = null;
|
|
810
|
-
try {
|
|
811
|
-
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
812
|
-
} catch (/** @type {unknown} */ err) {
|
|
813
|
-
error(fmt.status('WARN', `Failed to read verdict file: ${/** @type {Error} */(err).message}`));
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/** @type {{round: number, phase: string, actor: string, verdict?: string}} */
|
|
817
|
-
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
818
|
-
if (verdictContent && isValidVerdict(/** @type {string} */ (verdictContent))) {
|
|
819
|
-
outcomeParams.verdict = verdictContent;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const result = importLegacyArtifact(slug, 'review-verdict.txt', VALID_EVENT_TYPES.REVIEWER_OUTCOME,
|
|
823
|
-
outcomeParams,
|
|
824
|
-
{ ...options, worktree: rootDir });
|
|
825
|
-
|
|
826
|
-
if (result.ok) {
|
|
827
|
-
if (result.path) {
|
|
828
|
-
imported.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, path: result.path, legacyPath: result.legacyPath });
|
|
829
|
-
}
|
|
830
|
-
} else {
|
|
831
|
-
errors.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, error: result.error });
|
|
832
568
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
569
|
+
else if (verdictPath && fs.existsSync(verdictPath)) {
|
|
570
|
+
let verdictContent = null;
|
|
571
|
+
try {
|
|
572
|
+
verdictContent = fs.readFileSync(verdictPath, 'utf8').trim();
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
error(fmt.status('WARN', `Failed to read verdict file: ${err.message}`));
|
|
576
|
+
}
|
|
577
|
+
const outcomeParams = { round, phase, actor: 'legacy' };
|
|
578
|
+
if (verdictContent && isValidVerdict(verdictContent)) {
|
|
579
|
+
outcomeParams.verdict = verdictContent;
|
|
580
|
+
}
|
|
581
|
+
const result = importLegacyArtifact(slug, 'review-verdict.txt', VALID_EVENT_TYPES.REVIEWER_OUTCOME, outcomeParams, { ...options, worktree: rootDir });
|
|
582
|
+
if (result.ok) {
|
|
583
|
+
if (result.path) {
|
|
584
|
+
imported.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, path: result.path, legacyPath: result.legacyPath });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
errors.push({ artifactName: 'review-verdict.txt', eventType: VALID_EVENT_TYPES.REVIEWER_OUTCOME, error: result.error });
|
|
589
|
+
}
|
|
840
590
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
591
|
+
for (const artifactName of Object.keys(LEGACY_ARTIFACT_TO_EVENT_TYPE)) {
|
|
592
|
+
if (artifactName === 'review-outcome.md' || artifactName === 'review-verdict.txt') {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const result = importLegacyArtifact(slug, artifactName, LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], { round, phase, actor: 'legacy' }, { ...options, worktree: rootDir });
|
|
596
|
+
if (result.ok) {
|
|
597
|
+
if (result.path) {
|
|
598
|
+
imported.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], path: result.path, legacyPath: result.legacyPath });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
errors.push({ artifactName, eventType: LEGACY_ARTIFACT_TO_EVENT_TYPE[artifactName], error: result.error });
|
|
603
|
+
}
|
|
852
604
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return { ok: errors.length === 0, imported, errors };
|
|
605
|
+
return { ok: errors.length === 0, imported, errors };
|
|
856
606
|
}
|
|
857
|
-
|
|
858
607
|
// -------- Event Reading --------
|
|
859
|
-
|
|
860
608
|
/**
|
|
861
609
|
* Read all review events for a mission.
|
|
862
|
-
*
|
|
863
|
-
* @param {string} slug - Mission slug
|
|
864
|
-
* @param {Object} [options] - Options
|
|
865
|
-
* @param {string} [options.rootDir] - Root directory
|
|
866
|
-
* @param {Function} [options.readdirSync] - readdirSync override for testing
|
|
867
|
-
* @param {Function} [options.readFileSync] - readFileSync override for testing
|
|
868
|
-
* @returns {Array<any>} - Array of parsed event objects
|
|
869
|
-
*/
|
|
870
|
-
/**
|
|
871
|
-
* @param {string} slug
|
|
872
|
-
* @param {{rootDir?: string, readdirSync?: Function, readFileSync?: Function, error?: Function}} [options]
|
|
873
|
-
* @returns {Array<any>}
|
|
874
610
|
*/
|
|
875
611
|
function readAllEvents(slug, options = {}) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const eventsDir = reviewEventsDir(slug, rootDir);
|
|
884
|
-
if (!eventsDir || !fs.existsSync(eventsDir)) {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const events = [];
|
|
889
|
-
let files;
|
|
890
|
-
|
|
891
|
-
try {
|
|
892
|
-
files = readdirSync(eventsDir);
|
|
893
|
-
} catch (/** @type {unknown} */ err) {
|
|
894
|
-
error(fmt.status('WARN', `Failed to read review-events directory: ${/** @type {Error} */(err).message}`));
|
|
895
|
-
return [];
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
for (const filename of files) {
|
|
899
|
-
if (!filename.endsWith('.md')) {continue;}
|
|
900
|
-
|
|
901
|
-
const filePath = path.join(eventsDir, filename);
|
|
902
|
-
let content;
|
|
903
|
-
|
|
612
|
+
const { rootDir = process.cwd(), readdirSync = fs.readdirSync, readFileSync = fs.readFileSync, error = fmt.log.plainError } = options;
|
|
613
|
+
const eventsDir = reviewEventsDir(slug, rootDir);
|
|
614
|
+
if (!eventsDir || !fs.existsSync(eventsDir)) {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
const events = [];
|
|
618
|
+
let files;
|
|
904
619
|
try {
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
620
|
+
files = readdirSync(eventsDir);
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
error(fmt.status('WARN', `Failed to read review-events directory: ${err.message}`));
|
|
624
|
+
return [];
|
|
909
625
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
626
|
+
for (const filename of files) {
|
|
627
|
+
if (!filename.endsWith('.md')) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const filePath = path.join(eventsDir, filename);
|
|
631
|
+
let content;
|
|
632
|
+
try {
|
|
633
|
+
content = readFileSync(filePath, 'utf8');
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
error(fmt.status('WARN', `Failed to read event file ${filename}: ${err.message}`));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const event = parseEventFile(content, filePath);
|
|
640
|
+
if (event) {
|
|
641
|
+
events.push(event);
|
|
642
|
+
}
|
|
914
643
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
return bTime.localeCompare(aTime);
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
return events;
|
|
644
|
+
events.sort((a, b) => {
|
|
645
|
+
const aTime = a.timestamp || a.fileCreated || '';
|
|
646
|
+
const bTime = b.timestamp || b.fileCreated || '';
|
|
647
|
+
return bTime.localeCompare(aTime);
|
|
648
|
+
});
|
|
649
|
+
return events;
|
|
925
650
|
}
|
|
926
|
-
|
|
927
651
|
/**
|
|
928
652
|
* Parse an event file into a structured object.
|
|
929
|
-
* Extracts frontmatter metadata and content body.
|
|
930
|
-
*
|
|
931
|
-
* @param {string} content - File content
|
|
932
|
-
* @param {string} filePath - File path (for fallback metadata)
|
|
933
|
-
* @returns {Object|null} - Parsed event or null
|
|
934
|
-
*/
|
|
935
|
-
/**
|
|
936
|
-
* @param {string} content
|
|
937
|
-
* @param {string} filePath
|
|
938
|
-
* @returns {{[key: string]: any}|null}
|
|
939
653
|
*/
|
|
940
654
|
function parseEventFile(content, filePath) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
655
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
656
|
+
const metadata = {};
|
|
657
|
+
let body = content;
|
|
658
|
+
if (frontmatterMatch) {
|
|
659
|
+
const frontmatterText = frontmatterMatch[1];
|
|
660
|
+
body = content.slice(frontmatterMatch[0].length);
|
|
661
|
+
const lines = frontmatterText.split('\n');
|
|
662
|
+
for (const line of lines) {
|
|
663
|
+
if (!line.trim()) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const colonIdx = line.indexOf(':');
|
|
667
|
+
if (colonIdx === -1) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
671
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
672
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
673
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
674
|
+
metadata[key] = value.slice(1, -1);
|
|
675
|
+
}
|
|
676
|
+
else if (value.startsWith('[') && value.endsWith(']')) {
|
|
677
|
+
try {
|
|
678
|
+
metadata[key] = JSON.parse(value);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
metadata[key] = value;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else if (value === 'true') {
|
|
685
|
+
metadata[key] = true;
|
|
686
|
+
}
|
|
687
|
+
else if (value === 'false') {
|
|
688
|
+
metadata[key] = false;
|
|
689
|
+
}
|
|
690
|
+
else if (value !== '' && !isNaN(Number(value))) {
|
|
691
|
+
metadata[key] = Number(value);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
metadata[key] = value;
|
|
695
|
+
}
|
|
972
696
|
}
|
|
973
|
-
}
|
|
974
|
-
// Handle booleans and numbers
|
|
975
|
-
else if (value === 'true') {/** @type {any} */ (metadata)[key] = true;}
|
|
976
|
-
else if (value === 'false') {/** @type {any} */ (metadata)[key] = false;}
|
|
977
|
-
else if (value !== '' && !isNaN(Number(value))) {/** @type {any} */ (metadata)[key] = Number(value);}
|
|
978
|
-
else {/** @type {any} */ (metadata)[key] = value;}
|
|
979
697
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const possibleType = parts.slice(1, -2).join('-');
|
|
990
|
-
if (isValidEventType(possibleType)) {
|
|
991
|
-
metadata.event_type = possibleType;
|
|
992
|
-
}
|
|
698
|
+
if (!metadata.event_type && filePath) {
|
|
699
|
+
const basename = path.basename(filePath, '.md');
|
|
700
|
+
const parts = basename.split('-');
|
|
701
|
+
if (parts.length >= 4) {
|
|
702
|
+
const possibleType = parts.slice(1, -2).join('-');
|
|
703
|
+
if (isValidEventType(possibleType)) {
|
|
704
|
+
metadata.event_type = possibleType;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
993
707
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
filePath,
|
|
1003
|
-
fileCreated: stat.birthtime.toISOString(),
|
|
1004
|
-
fileModified: stat.mtime.toISOString()
|
|
1005
|
-
};
|
|
708
|
+
const stat = fs.statSync(filePath);
|
|
709
|
+
return {
|
|
710
|
+
...metadata,
|
|
711
|
+
content: body.trim(),
|
|
712
|
+
filePath,
|
|
713
|
+
fileCreated: stat.birthtime.toISOString(),
|
|
714
|
+
fileModified: stat.mtime.toISOString()
|
|
715
|
+
};
|
|
1006
716
|
}
|
|
1007
|
-
|
|
1008
|
-
const shouldMirrorToForgejo = shouldMirrorToProvider;
|
|
1009
|
-
|
|
1010
|
-
// -------- Module Exports --------
|
|
1011
|
-
|
|
1012
|
-
module.exports = {
|
|
1013
|
-
// Taxonomy constants
|
|
1014
|
-
VALID_EVENT_TYPES,
|
|
1015
|
-
ALL_EVENT_TYPES,
|
|
1016
|
-
MIRRORED_EVENT_TYPES,
|
|
1017
|
-
VALID_DISPOSITIONS,
|
|
1018
|
-
VALID_VERDICTS,
|
|
1019
|
-
|
|
1020
|
-
// Validation
|
|
1021
|
-
isValidEventType,
|
|
1022
|
-
isValidDisposition,
|
|
1023
|
-
isValidVerdict,
|
|
1024
|
-
shouldMirrorToProvider,
|
|
1025
|
-
shouldMirrorToForgejo,
|
|
1026
|
-
|
|
1027
|
-
// Path resolution
|
|
1028
|
-
reviewEventsDir,
|
|
1029
|
-
eventFilePath,
|
|
1030
|
-
legacyArtifactPath,
|
|
1031
|
-
|
|
1032
|
-
// Event creation
|
|
1033
|
-
createEvent,
|
|
1034
|
-
normalizeEventContent,
|
|
1035
|
-
|
|
1036
|
-
// Legacy compatibility
|
|
1037
|
-
importLegacyArtifact,
|
|
1038
|
-
importAllLegacyArtifacts,
|
|
1039
|
-
LEGACY_ARTIFACT_TO_EVENT_TYPE,
|
|
1040
|
-
|
|
1041
|
-
// Event reading
|
|
1042
|
-
readAllEvents,
|
|
1043
|
-
parseEventFile,
|
|
1044
|
-
|
|
1045
|
-
// Rendering
|
|
1046
|
-
buildEventFrontmatter,
|
|
1047
|
-
buildEventFooter,
|
|
1048
|
-
renderEventFile,
|
|
1049
|
-
|
|
1050
|
-
// Classification
|
|
1051
|
-
hasWorkflowFooter,
|
|
1052
|
-
classifyComment,
|
|
1053
|
-
consumeHumanNotes,
|
|
1054
|
-
|
|
1055
|
-
// Utilities
|
|
1056
|
-
generateEventTimestamp,
|
|
1057
|
-
sanitizeFilename,
|
|
1058
|
-
};
|