@besales/ops-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +328 -0
  3. package/bin/build-check-context.mjs +67 -0
  4. package/bin/build-execution-ledger.mjs +54 -0
  5. package/bin/estimate-llm-input.mjs +160 -0
  6. package/bin/guard-task.mjs +384 -0
  7. package/bin/hash-task-artifacts.mjs +44 -0
  8. package/bin/init-project.mjs +49 -0
  9. package/bin/intake-execution-feedback.mjs +207 -0
  10. package/bin/intake-feedback.test.mjs +73 -0
  11. package/bin/learning-loop.mjs +658 -0
  12. package/bin/learning-loop.test.mjs +175 -0
  13. package/bin/lib/bootstrap-utils.mjs +542 -0
  14. package/bin/lib/bootstrap-utils.test.mjs +156 -0
  15. package/bin/lib/check-context-utils.mjs +1448 -0
  16. package/bin/lib/check-context-utils.test.mjs +497 -0
  17. package/bin/lib/execution-ledger-utils.mjs +162 -0
  18. package/bin/lib/execution-ledger-utils.test.mjs +74 -0
  19. package/bin/lib/llm-input-pack-utils.mjs +663 -0
  20. package/bin/lib/llm-input-pack-utils.test.mjs +262 -0
  21. package/bin/lib/project-config.mjs +229 -0
  22. package/bin/lib/project-config.test.mjs +102 -0
  23. package/bin/lib/task-manifest-utils.mjs +512 -0
  24. package/bin/lib/task-manifest-utils.test.mjs +218 -0
  25. package/bin/lib/task-metrics-utils.mjs +63 -0
  26. package/bin/lib/task-metrics-utils.test.mjs +40 -0
  27. package/bin/lib/test-setup.mjs +37 -0
  28. package/bin/new-task.mjs +42 -0
  29. package/bin/ops-agent.mjs +81 -0
  30. package/bin/preflight.mjs +56 -0
  31. package/bin/providers/external-cli-checker.mjs +190 -0
  32. package/bin/providers/openai-checker.mjs +62 -0
  33. package/bin/quality-gates.mjs +92 -0
  34. package/bin/run-check.mjs +559 -0
  35. package/bin/run-plan-check-loop.mjs +392 -0
  36. package/bin/run-verify.mjs +627 -0
  37. package/bin/self-lint.mjs +88 -0
  38. package/bin/supervisor-turn.mjs +146 -0
  39. package/bin/supervisor-turn.test.mjs +72 -0
  40. package/bin/task-manifest.mjs +57 -0
  41. package/bin/task-metrics.mjs +48 -0
  42. package/bin/transition.mjs +94 -0
  43. package/bin/validate-check-artifacts.mjs +418 -0
  44. package/config/default-agents.json +100 -0
  45. package/package.json +28 -0
  46. package/playbooks/checker-context.md +9 -0
  47. package/playbooks/complexity-performance.md +13 -0
  48. package/playbooks/production-rollout.md +9 -0
  49. package/playbooks/source-sync-provider.md +9 -0
  50. package/playbooks/ui-acceptance.md +9 -0
  51. package/prompts/checker.md +170 -0
  52. package/prompts/executor.md +54 -0
  53. package/prompts/planner.md +128 -0
  54. package/prompts/researcher.md +44 -0
  55. package/prompts/supervisor.md +337 -0
  56. package/prompts/verifier.md +128 -0
  57. package/templates/brief.md +15 -0
  58. package/templates/check-resolution.md +69 -0
  59. package/templates/check-result.json +32 -0
  60. package/templates/check.md +46 -0
  61. package/templates/execution-feedback.md +25 -0
  62. package/templates/execution.md +101 -0
  63. package/templates/human-gate-summary.md +49 -0
  64. package/templates/orchestration-log.md +8 -0
  65. package/templates/plan.md +86 -0
  66. package/templates/research.md +13 -0
  67. package/templates/retrospective.md +48 -0
  68. package/templates/status.md +53 -0
  69. package/templates/verify-result.json +19 -0
  70. package/templates/verify.md +41 -0
@@ -0,0 +1,384 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import {
5
+ isCheckContextCurrent,
6
+ normalizeMarkdownBody,
7
+ pipelineRoot,
8
+ projectContext,
9
+ readJsonFile,
10
+ readStatusStage,
11
+ readTaskFile,
12
+ repoRoot,
13
+ resolveTaskDir,
14
+ sha256,
15
+ validateExecutionEvidenceForPlan,
16
+ } from './lib/check-context-utils.mjs';
17
+
18
+ function main() {
19
+ const taskArg = process.argv[2];
20
+ if (!taskArg) {
21
+ fail('Usage: node ops/agent-pipeline/bin/guard-task.mjs <TASK-id-or-task-path>');
22
+ }
23
+
24
+ const errors = [];
25
+ try {
26
+ const taskDir = resolveTaskDir(taskArg);
27
+ const taskId = path.basename(taskDir);
28
+ validateContext(taskDir, errors);
29
+ validatePlanCheckFreshness(taskDir, errors);
30
+ validateStructuredArtifacts(taskArg, errors);
31
+ validateCheckResult(taskDir, errors);
32
+ validateVerifyResult(taskDir, errors);
33
+ validateHumanGateSummary(taskDir, errors);
34
+ validateFeedback(taskDir, errors);
35
+ validateExecutionEvidence(taskDir, errors);
36
+ validateStatusSync(taskDir, errors);
37
+
38
+ if (errors.length > 0) {
39
+ console.error(`Agent pipeline guard failed for ${taskId}:`);
40
+ for (const error of errors) {
41
+ console.error(`- ${error}`);
42
+ }
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log(`Agent pipeline guard ok for ${taskId}`);
47
+ } catch (error) {
48
+ fail(error.message);
49
+ }
50
+ }
51
+
52
+ function validateHumanGateSummary(taskDir, errors) {
53
+ const stage = readStatusStage(taskDir);
54
+ if (stage !== 'Human Gate') {
55
+ return;
56
+ }
57
+ const summaryPath = path.join(taskDir, 'human-gate-summary.md');
58
+ if (!fs.existsSync(summaryPath)) {
59
+ errors.push('human-gate-summary.md is missing at Human Gate.');
60
+ return;
61
+ }
62
+ const summary = readTaskFile(taskDir, 'human-gate-summary.md');
63
+ for (const heading of [
64
+ '## Decision Needed',
65
+ '## Problem We Are Solving',
66
+ '## Approved Plan Summary',
67
+ '## Execution Approach',
68
+ '## Human Gate Requirements',
69
+ '## What Approval Allows',
70
+ '## What Remains Forbidden',
71
+ ]) {
72
+ if (!summary.includes(heading)) {
73
+ errors.push(`human-gate-summary.md is missing section: ${heading}.`);
74
+ }
75
+ }
76
+ }
77
+
78
+ function validatePlanCheckFreshness(taskDir, errors) {
79
+ const planPath = path.join(taskDir, 'plan.md');
80
+ const checkPath = path.join(taskDir, 'check.md');
81
+ if (!fs.existsSync(planPath)) {
82
+ errors.push('plan.md is missing.');
83
+ return;
84
+ }
85
+ if (!fs.existsSync(checkPath)) {
86
+ errors.push('check.md is missing.');
87
+ return;
88
+ }
89
+ const planMtime = fs.statSync(planPath).mtimeMs;
90
+ const checkMtime = fs.statSync(checkPath).mtimeMs;
91
+ if (planMtime > checkMtime) {
92
+ errors.push('plan.md is newer than check.md; fresh Check is required.');
93
+ }
94
+ }
95
+
96
+ function validateContext(taskDir, errors) {
97
+ const contextPath = path.join(taskDir, 'check-context.json');
98
+ if (!fs.existsSync(contextPath)) {
99
+ errors.push('check-context.json is missing.');
100
+ return;
101
+ }
102
+ const state = isCheckContextCurrent(taskDir);
103
+ if (!state.current) {
104
+ errors.push(state.error ? `check-context.json is stale or invalid: ${state.error.message}` : 'check-context.json is stale.');
105
+ }
106
+ }
107
+
108
+ function validateStructuredArtifacts(taskArg, errors) {
109
+ const validatorPath = path.join(pipelineRoot, 'bin', 'validate-check-artifacts.mjs');
110
+ const result = spawnSync(process.execPath, [validatorPath, taskArg], {
111
+ cwd: repoRoot,
112
+ encoding: 'utf8',
113
+ });
114
+ if (result.status !== 0) {
115
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
116
+ errors.push(`structured artifact validator failed: ${output}`);
117
+ }
118
+ }
119
+
120
+ function validateCheckResult(taskDir, errors) {
121
+ const resultPath = path.join(taskDir, 'check.result.json');
122
+ if (!fs.existsSync(resultPath)) {
123
+ errors.push('check.result.json is missing.');
124
+ return;
125
+ }
126
+
127
+ let result;
128
+ try {
129
+ result = readJsonFile(resultPath);
130
+ } catch (error) {
131
+ errors.push(`check.result.json is invalid JSON: ${error.message}`);
132
+ return;
133
+ }
134
+
135
+ if (result.verdict !== 'ready_for_human_gate') {
136
+ errors.push(`Human Gate is blocked by check.result.json verdict=${result.verdict}.`);
137
+ }
138
+ if (result.verdict === 'return_to_plan' && !fs.existsSync(path.join(taskDir, 'check-resolution.md'))) {
139
+ errors.push('check-resolution.md is missing after return_to_plan.');
140
+ }
141
+ if (result.verdict === 'checker_failed') {
142
+ errors.push(`checker_failed requires remediation before Human Gate: ${result.failureReason}.`);
143
+ }
144
+ if (result.verdict === 'human_arbitration_required') {
145
+ errors.push('Human Arbitration must resolve before Human Gate.');
146
+ }
147
+ }
148
+
149
+ function validateVerifyResult(taskDir, errors) {
150
+ const stage = readStatusStage(taskDir);
151
+ const resultPath = path.join(taskDir, 'verify.result.json');
152
+ const stagesRequiringVerifyResult = new Set([
153
+ 'Retrospective',
154
+ 'Closeout Audit',
155
+ 'Human Closeout Gate',
156
+ 'Closed',
157
+ 'Accepted',
158
+ 'Ready To Pause',
159
+ ]);
160
+
161
+ if (!fs.existsSync(resultPath)) {
162
+ if (stagesRequiringVerifyResult.has(stage)) {
163
+ errors.push('verify.result.json is missing before Retrospective/Closeout.');
164
+ }
165
+ return;
166
+ }
167
+
168
+ let result;
169
+ try {
170
+ result = readJsonFile(resultPath);
171
+ } catch (error) {
172
+ errors.push(`verify.result.json is invalid JSON: ${error.message}`);
173
+ return;
174
+ }
175
+
176
+ const allowedVerdicts = new Set([
177
+ 'pass',
178
+ 'pass_with_notes',
179
+ 'return_to_execute',
180
+ 'return_to_plan',
181
+ 'human_arbitration_required',
182
+ 'verifier_failed',
183
+ ]);
184
+ const allowedFailureReasons = new Set([
185
+ 'provider_unavailable',
186
+ 'invalid_json',
187
+ 'schema_validation_failed',
188
+ 'context_overflow',
189
+ 'repo_read_limit_exceeded',
190
+ 'memory_snapshot_mismatch',
191
+ 'timeout',
192
+ 'unknown',
193
+ ]);
194
+ const allowedSeverities = new Set(['blocking', 'non_blocking', 'question']);
195
+ const allowedCategories = new Set([
196
+ 'plan_mismatch',
197
+ 'implementation_gap',
198
+ 'regression_risk',
199
+ 'insufficient_evidence',
200
+ 'unrun_required_check',
201
+ 'scope_drift',
202
+ 'runtime_risk',
203
+ 'ui_verification_gap',
204
+ 'documentation_gap',
205
+ 'execution_record_gap',
206
+ 'verifier_failure',
207
+ ]);
208
+ const externalVerifierProviders = readExternalVerifierProviders();
209
+ const verificationMode = result.verificationMode;
210
+
211
+ if (result.schemaVersion !== 1) {
212
+ errors.push('verify.result.json schemaVersion must be 1.');
213
+ }
214
+ if (!allowedVerdicts.has(result.verdict)) {
215
+ errors.push(`verify.result.json has invalid verdict: ${result.verdict}.`);
216
+ }
217
+ if (result.verdict === 'verifier_failed' && !allowedFailureReasons.has(result.failureReason)) {
218
+ errors.push(`verify.result.json has invalid failureReason: ${result.failureReason}.`);
219
+ }
220
+ if (result.verdict !== 'verifier_failed' && result.failureReason !== null) {
221
+ errors.push('verify.result.json failureReason must be null unless verdict=verifier_failed.');
222
+ }
223
+ if (['pass', 'pass_with_notes'].includes(result.verdict)) {
224
+ if (!['internal_supervisor', 'external_cli'].includes(verificationMode)) {
225
+ errors.push('verify.result.json pass/pass_with_notes requires verificationMode="internal_supervisor" or "external_cli".');
226
+ }
227
+ if (verificationMode === 'internal_supervisor' && result.verifierProvider !== 'supervisor') {
228
+ errors.push(`verify.result.json internal_supervisor requires verifierProvider="supervisor", got ${result.verifierProvider}.`);
229
+ }
230
+ if (verificationMode === 'external_cli' && !externalVerifierProviders.has(result.verifierProvider)) {
231
+ errors.push(`verify.result.json external_cli verifierProvider must be an external CLI provider (${[...externalVerifierProviders].join(', ')}), got ${result.verifierProvider}.`);
232
+ }
233
+ if (!result.verifierRunId || typeof result.verifierRunId !== 'string') {
234
+ errors.push('verify.result.json pass/pass_with_notes requires verifierRunId.');
235
+ }
236
+ }
237
+
238
+ const planPath = path.join(taskDir, 'plan.md');
239
+ if (fs.existsSync(planPath)) {
240
+ const planSha = sha256(normalizeMarkdownBody(fs.readFileSync(planPath, 'utf8')));
241
+ if (result.planSha && result.planSha !== planSha) {
242
+ errors.push('verify.result.json is stale relative to plan.md.');
243
+ }
244
+ }
245
+
246
+ const executionPath = path.join(taskDir, 'execution.md');
247
+ if (fs.existsSync(executionPath)) {
248
+ const executionSha = sha256(normalizeMarkdownBody(fs.readFileSync(executionPath, 'utf8')));
249
+ if (result.executionSha && result.executionSha !== executionSha) {
250
+ errors.push('verify.result.json is stale relative to execution.md.');
251
+ }
252
+ }
253
+
254
+ const findings = Array.isArray(result.findings) ? result.findings : [];
255
+ if (!Array.isArray(result.findings)) {
256
+ errors.push('verify.result.json findings must be an array.');
257
+ }
258
+ findings.forEach((finding, index) => {
259
+ const expectedId = `V-${String(index + 1).padStart(3, '0')}`;
260
+ if (finding.id !== expectedId) {
261
+ errors.push(`verify.result.json finding id must be stable/sequential: expected ${expectedId}, got ${finding.id}.`);
262
+ }
263
+ if (!allowedSeverities.has(finding.severity)) {
264
+ errors.push(`verify.result.json finding ${finding.id || index + 1} has invalid severity.`);
265
+ }
266
+ if (!allowedCategories.has(finding.claimCategory)) {
267
+ errors.push(`verify.result.json finding ${finding.id || index + 1} has invalid claimCategory.`);
268
+ }
269
+ });
270
+
271
+ const blockingFindings = findings.filter((finding) => finding.severity === 'blocking').length;
272
+ const nonBlockingFindings = findings.filter((finding) => finding.severity === 'non_blocking').length;
273
+ const questions = findings.filter((finding) => finding.severity === 'question').length;
274
+ if (result.counts?.blockingFindings !== blockingFindings) {
275
+ errors.push('verify.result.json counts.blockingFindings does not match findings[].');
276
+ }
277
+ if (result.counts?.nonBlockingFindings !== nonBlockingFindings) {
278
+ errors.push('verify.result.json counts.nonBlockingFindings does not match findings[].');
279
+ }
280
+ if (result.counts?.questions !== questions) {
281
+ errors.push('verify.result.json counts.questions does not match findings[].');
282
+ }
283
+
284
+ if (['pass', 'pass_with_notes'].includes(result.verdict) && blockingFindings > 0) {
285
+ errors.push('verify.result.json cannot pass with blocking findings.');
286
+ }
287
+ if (result.readyForRetrospective !== ['pass', 'pass_with_notes'].includes(result.verdict)) {
288
+ errors.push('verify.result.json readyForRetrospective disagrees with verdict.');
289
+ }
290
+
291
+ if (stagesRequiringVerifyResult.has(stage) && !['pass', 'pass_with_notes'].includes(result.verdict)) {
292
+ errors.push(`Retrospective/Closeout is blocked by verify.result.json verdict=${result.verdict}.`);
293
+ }
294
+ }
295
+
296
+ function readExternalVerifierProviders() {
297
+ const configPath = projectContext.projectAgentsConfigPath || path.join(projectContext.configRoot, 'default-agents.json');
298
+ const config = fs.existsSync(configPath) ? readJsonFile(configPath) : {};
299
+ const providerNames = Object.keys(config.checkerProviders || {});
300
+ const cliProviders = providerNames.filter((providerName) => providerName.endsWith('-cli') || providerName === 'custom-cli');
301
+ const configuredVerifierProvider = config.verifier?.provider;
302
+ if (configuredVerifierProvider && !cliProviders.includes(configuredVerifierProvider)) {
303
+ cliProviders.push(configuredVerifierProvider);
304
+ }
305
+ return new Set(cliProviders);
306
+ }
307
+
308
+ function validateFeedback(taskDir, errors) {
309
+ const feedbackPath = path.join(taskDir, 'feedback.md');
310
+ const legacyFeedbackPath = path.join(taskDir, 'execution-feedback.md');
311
+ if (!fs.existsSync(feedbackPath) && !fs.existsSync(legacyFeedbackPath)) {
312
+ return;
313
+ }
314
+ const feedback = [
315
+ readTaskFile(taskDir, 'feedback.md'),
316
+ readTaskFile(taskDir, 'execution-feedback.md'),
317
+ ].join('\n');
318
+ if (/Classification:\s*(pending|todo|unclassified|\.\.\.)/i.test(feedback) || !/Classification:\s*\w+/i.test(feedback)) {
319
+ errors.push('Task has unclassified feedback event. Run ops-agent intake-feedback or classify feedback before continuing.');
320
+ }
321
+ }
322
+
323
+ function validateStatusSync(taskDir, errors) {
324
+ const resultPath = path.join(taskDir, 'check.result.json');
325
+ if (!fs.existsSync(resultPath)) {
326
+ return;
327
+ }
328
+ const result = readJsonFile(resultPath);
329
+ const stage = readStatusStage(taskDir);
330
+ if (!stage) {
331
+ errors.push('status.md current stage is empty.');
332
+ return;
333
+ }
334
+
335
+ const allowedStages = {
336
+ ready_for_human_gate: new Set([
337
+ 'Human Gate',
338
+ 'Execute',
339
+ 'Verify',
340
+ 'Retrospective',
341
+ 'Closeout Audit',
342
+ 'Human Closeout Gate',
343
+ 'Closed',
344
+ 'Accepted',
345
+ 'Ready To Pause',
346
+ ]),
347
+ return_to_plan: new Set(['Plan']),
348
+ human_arbitration_required: new Set(['Human Arbitration']),
349
+ checker_failed: new Set(['Check']),
350
+ }[result.verdict];
351
+
352
+ if (allowedStages && !allowedStages.has(stage)) {
353
+ errors.push(`status.md stage "${stage}" disagrees with check verdict ${result.verdict}.`);
354
+ }
355
+ }
356
+
357
+ function validateExecutionEvidence(taskDir, errors) {
358
+ const stage = readStatusStage(taskDir);
359
+ const stagesAfterExecute = new Set([
360
+ 'Verify',
361
+ 'Retrospective',
362
+ 'Closeout Audit',
363
+ 'Human Closeout Gate',
364
+ 'Closed',
365
+ 'Accepted',
366
+ 'Ready To Pause',
367
+ ]);
368
+ if (!stagesAfterExecute.has(stage)) {
369
+ return;
370
+ }
371
+
372
+ const plan = readTaskFile(taskDir, 'plan.md');
373
+ const execution = readTaskFile(taskDir, 'execution.md');
374
+ for (const issue of validateExecutionEvidenceForPlan({ planContent: plan, executionContent: execution })) {
375
+ errors.push(issue.message);
376
+ }
377
+ }
378
+
379
+ function fail(message) {
380
+ console.error(`Error: ${message}`);
381
+ process.exit(1);
382
+ }
383
+
384
+ main();
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ computePlanFingerprint,
5
+ normalizeMarkdownBody,
6
+ parseCliArgs,
7
+ resolveTaskDir,
8
+ sha256,
9
+ } from './lib/check-context-utils.mjs';
10
+
11
+ function main() {
12
+ const args = parseCliArgs(process.argv.slice(2));
13
+ const taskArg = args.positional[0];
14
+ if (!taskArg) {
15
+ throw new Error('Usage: node ops/agent-pipeline/bin/hash-task-artifacts.mjs <TASK-id-or-task-path> [file ...]');
16
+ }
17
+
18
+ const taskDir = resolveTaskDir(taskArg);
19
+ const fileNames = args.positional.length > 1 ? args.positional.slice(1) : ['plan.md', 'execution.md'];
20
+ const artifacts = fileNames.map((fileName) => hashTaskFile(taskDir, fileName));
21
+
22
+ console.log(JSON.stringify({
23
+ taskId: path.basename(taskDir),
24
+ hashMode: 'pipeline-normalized-markdown',
25
+ note: 'These hashes match run-verify/guard-task markdown normalization; raw shasum is intentionally different.',
26
+ artifacts,
27
+ }, null, 2));
28
+ }
29
+
30
+ function hashTaskFile(taskDir, fileName) {
31
+ const filePath = path.join(taskDir, fileName);
32
+ if (!fs.existsSync(filePath)) {
33
+ throw new Error(`Task artifact not found: ${fileName}`);
34
+ }
35
+
36
+ const content = fs.readFileSync(filePath, 'utf8');
37
+ return {
38
+ fileName,
39
+ normalizedSha: sha256(normalizeMarkdownBody(content)),
40
+ ...(fileName === 'plan.md' ? { semanticPlanSha: computePlanFingerprint(content).planSha } : {}),
41
+ };
42
+ }
43
+
44
+ main();
@@ -0,0 +1,49 @@
1
+ import path from 'node:path';
2
+ import {
3
+ getFlag,
4
+ parseCliArgs,
5
+ } from './lib/check-context-utils.mjs';
6
+ import {
7
+ initProject,
8
+ summarizeChanges,
9
+ } from './lib/bootstrap-utils.mjs';
10
+
11
+ function main() {
12
+ const args = parseCliArgs(process.argv.slice(2));
13
+ const projectRoot = process.cwd();
14
+ const projectName = getFlag(args, 'project-name', path.basename(projectRoot));
15
+ const opsRoot = getFlag(args, 'ops-root', 'ops');
16
+ const installScripts = args.flags.has('install-scripts');
17
+ const frameworkPackage = getFlag(args, 'framework-package', null);
18
+ const frameworkVersion = getFlag(args, 'framework-version', null);
19
+ const force = args.flags.has('force');
20
+
21
+ try {
22
+ const result = initProject({
23
+ projectRoot,
24
+ projectName,
25
+ opsRoot,
26
+ installScripts,
27
+ frameworkPackage,
28
+ frameworkVersion,
29
+ force,
30
+ });
31
+ const summary = summarizeChanges(result.changes);
32
+
33
+ console.log(`Ops project initialized: ${result.projectName}`);
34
+ console.log(`- projectRoot: ${result.projectRoot}`);
35
+ console.log(`- opsRoot: ${result.opsRoot}`);
36
+ console.log(`- created: ${summary.created}`);
37
+ console.log(`- existing: ${summary.existing}`);
38
+ console.log(`- overwritten: ${summary.overwritten}`);
39
+ } catch (error) {
40
+ fail(error.message);
41
+ }
42
+ }
43
+
44
+ function fail(message) {
45
+ console.error(`Error: ${message}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ main();
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ appendOrchestrationLog,
6
+ getFlag,
7
+ parseCliArgs,
8
+ readStatusStage,
9
+ readTaskFile,
10
+ resolveTaskDir,
11
+ updateStatus,
12
+ writeTaskFile,
13
+ } from './lib/check-context-utils.mjs';
14
+
15
+ export function main() {
16
+ const args = parseCliArgs(process.argv.slice(2));
17
+ const taskArg = args.positional[0];
18
+ if (!taskArg) {
19
+ fail('Usage: ops-agent intake-feedback <TASK-id-or-task-path> <feedback> [--from-file path] [--source user|assistant|tool]');
20
+ }
21
+
22
+ try {
23
+ const taskDir = resolveTaskDir(taskArg);
24
+ const taskId = path.basename(taskDir);
25
+ const dryRun = getFlag(args, 'dry-run', false) === true;
26
+ const source = getFlag(args, 'source', 'user');
27
+ const rawFeedback = readFeedback(args);
28
+ if (!rawFeedback.trim()) {
29
+ throw new Error('Feedback is empty.');
30
+ }
31
+
32
+ const classification = classifyFeedback(rawFeedback);
33
+ if (dryRun) {
34
+ console.log(`Feedback dry-run for ${taskId}`);
35
+ console.log(`- classification: ${classification.classification}`);
36
+ console.log(`- supervisorDecision: ${classification.supervisorDecision}`);
37
+ console.log(`- requiresFreshCheck: ${classification.requiresFreshCheck ? 'yes' : 'no'}`);
38
+ return;
39
+ }
40
+
41
+ recordFeedback({ taskDir, taskId, rawFeedback, classification, source });
42
+
43
+ console.log(`Feedback recorded for ${taskId}`);
44
+ console.log(`- classification: ${classification.classification}`);
45
+ console.log(`- supervisorDecision: ${classification.supervisorDecision}`);
46
+ console.log(`- requiresFreshCheck: ${classification.requiresFreshCheck ? 'yes' : 'no'}`);
47
+ } catch (error) {
48
+ fail(error.message);
49
+ }
50
+ }
51
+
52
+ function readFeedback(args) {
53
+ const fromFile = getFlag(args, 'from-file');
54
+ if (fromFile) {
55
+ return fs.readFileSync(path.resolve(fromFile), 'utf8');
56
+ }
57
+ return args.positional.slice(1).join(' ');
58
+ }
59
+
60
+ export function classifyFeedback(rawFeedback) {
61
+ const text = rawFeedback.toLowerCase();
62
+
63
+ if (/^(да[\s,]+давай|да|давай|ок|окей|yes|yep|go|go ahead|approved|approve|подтверждаю|согласен)[\s.,!?]*$/i.test(rawFeedback.trim())) {
64
+ return classificationResult('acknowledgement', 'stay_current', false, false, 'Human acknowledged or approved the current supervisor-proposed next step.');
65
+ }
66
+ if (/\b(feedback|memory|playbook|learning|retrospective|framework|supervisor|status|log|artifact|фидбэк|памят|плейбук|лернинг|ретроспектив|фреймворк|супервизор|статус|лог|артефакт)\b/.test(text)) {
67
+ return classificationResult('learning_capture', 'stay_current', false, false, 'Feedback should be captured for retrospective, memory or playbook learning without changing current task scope by itself.');
68
+ }
69
+ if (/\b(scope|цель|outcome|dashboard|data model|canonical|lifecycle|новая задача|новый контур|границ[аы])\b/.test(text)) {
70
+ return classificationResult('brief_reset_required', 'return_brief', true, true, 'Feedback changes task frame or product/data-model boundary.');
71
+ }
72
+ if (/\b(unclear|unknown|research|исслед|проверь факты|provider behavior|production data|impact|непонятно)\b/.test(text)) {
73
+ return classificationResult('research_required', 'return_research', true, true, 'Feedback reveals missing facts.');
74
+ }
75
+ if (/\b(risk appetite|tradeoff|trade-off|accept risk|architecture decision|реши|выбрать|компромисс|риск)\b/.test(text)) {
76
+ return classificationResult('human_arbitration_required', 'human_arbitration', false, false, 'Feedback requires human risk or architecture arbitration.');
77
+ }
78
+ if (/\b(plan|implementation|file|module|verification|api|route|endpoint|worker|queue|schema|migration|auth|deploy|runtime|status|log|artifact|retrospective|memory|playbook|план|файл|модуль|провер|реализац|воркер|схем|миграц|статус|лог|артефакт|ретроспектив|памят|плейбук)\b/.test(text)) {
79
+ return classificationResult('plan_patch_required', 'return_plan', true, true, 'Feedback changes implementation path or verification surface.');
80
+ }
81
+ if (/\b(copy|text|label|typo|spacing|цвет|текст|опечат|лейбл|полиш|polish)\b/.test(text)) {
82
+ return classificationResult('within_approved_scope', 'stay_execute', false, false, 'Feedback appears to be within approved execution scope.');
83
+ }
84
+
85
+ return classificationResult('human_triage_required', 'human_triage', false, false, 'Feedback is ambiguous and needs classification.');
86
+ }
87
+
88
+ function classificationResult(classification, supervisorDecision, requiresFreshCheck, requiresNewHumanGate, rationale) {
89
+ return {
90
+ classification,
91
+ supervisorDecision,
92
+ requiresFreshCheck,
93
+ requiresNewHumanGate,
94
+ rationale,
95
+ };
96
+ }
97
+
98
+ export function appendFeedbackEvent({ taskDir, taskId, rawFeedback, classification, source = 'user' }) {
99
+ const fileName = 'feedback.md';
100
+ const statusStage = readStatusStage(taskDir) || 'unknown';
101
+ const existing = readTaskFile(taskDir, fileName).trimEnd() || '# Feedback Log';
102
+ const eventNumber = (existing.match(/^## Feedback Event /gm) || []).length + 1;
103
+ const receivedAt = new Date().toISOString();
104
+ const event = [
105
+ `## Feedback Event ${eventNumber}`,
106
+ '',
107
+ `- Received at: ${receivedAt}`,
108
+ `- Source: ${source}`,
109
+ `- Raw feedback: ${rawFeedback.replace(/\n/g, ' ')}`,
110
+ `- Current stage when received: ${statusStage}`,
111
+ `- Classification: ${classification.classification}`,
112
+ `- Rationale: ${classification.rationale}`,
113
+ '- Affected artifacts: plan.md, research.md, brief.md, execution.md, verify.md',
114
+ `- Supervisor decision: ${classification.supervisorDecision}`,
115
+ `- Requires fresh Check: ${classification.requiresFreshCheck ? 'yes' : 'no'}`,
116
+ `- Requires new Human Gate: ${classification.requiresNewHumanGate ? 'yes' : 'no'}`,
117
+ '',
118
+ '```json',
119
+ JSON.stringify({
120
+ event: 'feedback',
121
+ taskId,
122
+ receivedAt,
123
+ source,
124
+ rawFeedback,
125
+ stageWhenReceived: statusStage,
126
+ classification: classification.classification,
127
+ rationale: classification.rationale,
128
+ supervisorDecision: classification.supervisorDecision,
129
+ requiresFreshCheck: classification.requiresFreshCheck,
130
+ requiresNewHumanGate: classification.requiresNewHumanGate,
131
+ }, null, 2),
132
+ '```',
133
+ ].join('\n');
134
+
135
+ writeTaskFile(taskDir, fileName, `${existing}\n\n${event}\n`);
136
+ }
137
+
138
+ export function recordFeedback({ taskDir, taskId = path.basename(taskDir), rawFeedback, classification = classifyFeedback(rawFeedback), source = 'user' }) {
139
+ appendFeedbackEvent({ taskDir, taskId, rawFeedback, classification, source });
140
+ updateStatusForFeedback(taskDir, classification);
141
+ appendOrchestrationLog(taskDir, {
142
+ event: 'feedback_intake',
143
+ taskId,
144
+ source,
145
+ classification: classification.classification,
146
+ supervisorDecision: classification.supervisorDecision,
147
+ requiresFreshCheck: classification.requiresFreshCheck,
148
+ requiresNewHumanGate: classification.requiresNewHumanGate,
149
+ });
150
+ return classification;
151
+ }
152
+
153
+ export function updateStatusForFeedback(taskDir, classification) {
154
+ const currentStage = readStatusStage(taskDir) || 'Human Triage';
155
+ const stageByDecision = {
156
+ stay_execute: 'Execute',
157
+ stay_current: currentStage,
158
+ return_plan: 'Plan',
159
+ return_research: 'Research',
160
+ return_brief: 'Brief',
161
+ human_triage: 'Human Triage',
162
+ human_arbitration: 'Human Arbitration',
163
+ };
164
+
165
+ updateStatus(taskDir, {
166
+ stage: stageByDecision[classification.supervisorDecision] || 'Human Triage',
167
+ routingDecision: `feedback:${classification.classification}`,
168
+ feedbackIntake: '- `feedback.md`: classified',
169
+ supervisorAction: classification.rationale,
170
+ nextStep: nextStepForClassification(classification),
171
+ humanApproval: classification.requiresNewHumanGate ? 'yes, after fresh Check' : 'no',
172
+ });
173
+ }
174
+
175
+ function nextStepForClassification(classification) {
176
+ if (classification.classification === 'within_approved_scope') {
177
+ return 'Continue Execute within approved plan.';
178
+ }
179
+ if (classification.classification === 'learning_capture') {
180
+ return 'Keep current task stage; include feedback in retrospective and learning candidates.';
181
+ }
182
+ if (classification.classification === 'acknowledgement') {
183
+ return 'Proceed with the current supervisor-proposed next step.';
184
+ }
185
+ if (classification.classification === 'plan_patch_required') {
186
+ return 'Planner updates plan.md, then fresh Check and Human Gate before Execute resumes.';
187
+ }
188
+ if (classification.classification === 'research_required') {
189
+ return 'Researcher updates research.md, then Planner updates plan.md and fresh Check runs.';
190
+ }
191
+ if (classification.classification === 'brief_reset_required') {
192
+ return 'Update brief.md before Research, Plan, fresh Check and Human Gate.';
193
+ }
194
+ if (classification.classification === 'human_arbitration_required') {
195
+ return 'Human decides unresolved tradeoff before implementation continues.';
196
+ }
197
+ return 'Human classifies feedback before implementation continues.';
198
+ }
199
+
200
+ function fail(message) {
201
+ console.error(`Error: ${message}`);
202
+ process.exit(1);
203
+ }
204
+
205
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
206
+ main();
207
+ }