@formigio/fazemos-cli 0.10.12 → 0.10.14
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/dist/index.js +317 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,6 +22,56 @@ function parseNumber(val) {
|
|
|
22
22
|
throw new Error(`Invalid number: ${val}`);
|
|
23
23
|
return n;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* OPS-2 — Manual Interventions KPI cell formatter.
|
|
27
|
+
*
|
|
28
|
+
* Single source of CLI truth for the tri-state coloring rule encoded in
|
|
29
|
+
* rul_intervention_chip_thresholds_v1. Mirrors the web InterventionChip
|
|
30
|
+
* thresholds so the CLI MI column on `pl list`, the header line on
|
|
31
|
+
* `pl show`, and the `ops interventions` table all render the same way:
|
|
32
|
+
*
|
|
33
|
+
* null → `—` (no color) — pre-migration row or unknown state
|
|
34
|
+
* 0 → green — clean run
|
|
35
|
+
* 1-3 → amber — some operator help
|
|
36
|
+
* ≥4 → red — carried
|
|
37
|
+
* >999 → `999+` — cap per rul_intervention_chip_count_cap_999plus
|
|
38
|
+
*
|
|
39
|
+
* Width: 5 chars (right-padded inside the chalk color call so colors don't
|
|
40
|
+
* count against the visible width). Callers can wrap with additional text
|
|
41
|
+
* (e.g. `MI:${cell}`) without breaking alignment.
|
|
42
|
+
*/
|
|
43
|
+
function formatManualInterventionCell(count) {
|
|
44
|
+
if (count === null || count === undefined) {
|
|
45
|
+
return chalk.gray('—'.padEnd(5));
|
|
46
|
+
}
|
|
47
|
+
const display = count > 999 ? '999+' : String(count);
|
|
48
|
+
const padded = display.padEnd(5);
|
|
49
|
+
if (count === 0)
|
|
50
|
+
return chalk.green(padded);
|
|
51
|
+
if (count <= 3)
|
|
52
|
+
return chalk.yellow(padded);
|
|
53
|
+
return chalk.red(padded);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OPS-2 — parse a duration string ('24h', '7d', '30d', '90d', etc.) into
|
|
57
|
+
* an ISO-8601 datetime `since` value for the GET /api/ops/manual-interventions
|
|
58
|
+
* `since` query param.
|
|
59
|
+
*
|
|
60
|
+
* Accepted units: `h` (hours), `d` (days), `w` (weeks). Unrecognised values
|
|
61
|
+
* throw and surface to the operator via the standard error path.
|
|
62
|
+
*/
|
|
63
|
+
function parseSinceDuration(val) {
|
|
64
|
+
const m = /^(\d+)([hdw])$/.exec(val.trim());
|
|
65
|
+
if (!m) {
|
|
66
|
+
throw new Error(`Invalid --since "${val}". Expected forms like 24h, 7d, 30d, 4w.`);
|
|
67
|
+
}
|
|
68
|
+
const n = Number(m[1]);
|
|
69
|
+
const unit = m[2];
|
|
70
|
+
const ms = unit === 'h' ? n * 3_600_000
|
|
71
|
+
: unit === 'd' ? n * 86_400_000
|
|
72
|
+
: n * 604_800_000;
|
|
73
|
+
return new Date(Date.now() - ms).toISOString();
|
|
74
|
+
}
|
|
25
75
|
function parseStreamSilenceAbortMs(val) {
|
|
26
76
|
const n = Number(val);
|
|
27
77
|
if (!Number.isInteger(n) || n < 30000 || n > 1800000) {
|
|
@@ -2469,14 +2519,58 @@ commitments
|
|
|
2469
2519
|
process.exit(1);
|
|
2470
2520
|
}
|
|
2471
2521
|
});
|
|
2522
|
+
// [F24 §3.6 / R16-R20] cm reopen — Restore an open state on a closed
|
|
2523
|
+
// commitment. The close event stays in the audit trail (a new audit_log
|
|
2524
|
+
// row records the reopen with from_state and to_state='open'). Agents
|
|
2525
|
+
// cannot reopen — the API's permission predicate naturally excludes
|
|
2526
|
+
// service principals (requireMemberId throws when req.member.id is null).
|
|
2527
|
+
commitments
|
|
2528
|
+
.command('reopen <id>')
|
|
2529
|
+
.description('Restore an open state on a closed commitment. The close event stays in the audit trail.')
|
|
2530
|
+
.option('--reason <text>', 'Optional human-readable note for the audit log (max 500 chars)')
|
|
2531
|
+
.option('--json', 'Print the raw API response as JSON (machine-readable)')
|
|
2532
|
+
.action(async (id, opts) => {
|
|
2533
|
+
try {
|
|
2534
|
+
const body = {};
|
|
2535
|
+
if (opts.reason)
|
|
2536
|
+
body.reason = opts.reason;
|
|
2537
|
+
const data = await api('PATCH', `/api/commitments/${id}/reopen`, body);
|
|
2538
|
+
if (opts.json) {
|
|
2539
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
if (data.noop) {
|
|
2543
|
+
console.log(chalk.yellow('This commitment is already open. Nothing to reopen.'));
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
const c = data.commitment;
|
|
2547
|
+
const prev = data.previousCompletedAt;
|
|
2548
|
+
const prevFmt = prev ? new Date(prev).toISOString().slice(0, 16).replace('T', ' ') : null;
|
|
2549
|
+
const tail = prevFmt ? ` The close on ${prevFmt} stays in the audit trail.` : '';
|
|
2550
|
+
console.log(chalk.green(`Reopened. Commitment ${c.id} is open again.${tail}`));
|
|
2551
|
+
}
|
|
2552
|
+
catch (err) {
|
|
2553
|
+
console.error(chalk.red(err.message));
|
|
2554
|
+
process.exit(1);
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
// [F24 §3.4 / R13-R14] cm execute — gained `--repos` option for
|
|
2558
|
+
// commitment-source clone overrides (mirrors the top-level execute flag).
|
|
2559
|
+
// The server-side endpoint now reads agent_config.repos as a fallback
|
|
2560
|
+
// when no body.repos is supplied; this CLI flag is the operator-side
|
|
2561
|
+
// override for `fazemos cm execute -w <ws> -c <cm> --repos <repos>`.
|
|
2472
2562
|
commitments
|
|
2473
2563
|
.command('execute')
|
|
2474
2564
|
.description('Trigger an agent execution for a commitment. The API determines which agent to use based on the commitment\'s configuration. For more control, use the top-level "execute" command instead.')
|
|
2475
2565
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
2476
2566
|
.requiredOption('-c, --commitment <id>', 'Commitment ID (from "cm list" or "ws show" output)')
|
|
2567
|
+
.option('--repos <repos>', 'Comma-separated repo names to clone for this execution (overrides the agent\'s default agent_config.repos)', (v) => v.split(','))
|
|
2477
2568
|
.action(async (opts) => {
|
|
2478
2569
|
try {
|
|
2479
|
-
const
|
|
2570
|
+
const body = {};
|
|
2571
|
+
if (opts.repos)
|
|
2572
|
+
body.repos = opts.repos;
|
|
2573
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments/${opts.commitment}/execute`, body);
|
|
2480
2574
|
console.log(chalk.green('Execution triggered'));
|
|
2481
2575
|
if (data.execution?.id)
|
|
2482
2576
|
console.log(` Execution ID: ${data.execution.id}`);
|
|
@@ -3956,7 +4050,18 @@ pipelines
|
|
|
3956
4050
|
}
|
|
3957
4051
|
for (const inst of data.instances) {
|
|
3958
4052
|
const stepInfo = inst.total_steps ? ` — step ${inst.current_step ?? '?'}/${inst.total_steps}` : '';
|
|
3959
|
-
|
|
4053
|
+
// OPS-2 — Manual Interventions KPI. The MI column renders inline next
|
|
4054
|
+
// to the instance status with the prescribed tri-state coloring:
|
|
4055
|
+
// null → `—` (no color; pre-migration row or unknown)
|
|
4056
|
+
// 0 → green
|
|
4057
|
+
// 1-3 → amber
|
|
4058
|
+
// ≥4 → red
|
|
4059
|
+
// Capped per rul_intervention_chip_count_cap_999plus: values > 999
|
|
4060
|
+
// collapse to `999+` while the underlying count remains intact in
|
|
4061
|
+
// the API response and JSON output for scripts.
|
|
4062
|
+
const miCell = formatManualInterventionCell(inst.manual_intervention_count);
|
|
4063
|
+
const envCell = inst.env_tag ? chalk.gray(` [${inst.env_tag}]`) : '';
|
|
4064
|
+
console.log(` ${chalk.cyan(inst.name)} (${inst.status}) MI:${miCell}${envCell}${stepInfo}`);
|
|
3960
4065
|
console.log(` ID: ${inst.id}`);
|
|
3961
4066
|
if (opts.expand && inst.steps?.length) {
|
|
3962
4067
|
for (const s of inst.steps) {
|
|
@@ -3985,6 +4090,14 @@ pipelines
|
|
|
3985
4090
|
console.log(` ID: ${inst.id}`);
|
|
3986
4091
|
console.log(` Status: ${inst.status}`);
|
|
3987
4092
|
console.log(` Template: ${inst.template_name || inst.template_id}`);
|
|
4093
|
+
// OPS-2 — Manual Interventions KPI header line. Same coloring rule as
|
|
4094
|
+
// `pl list` (null=—, 0=green, 1-3=amber, 4+=red). env_tag is surfaced
|
|
4095
|
+
// alongside so operators can read prod-vs-dev at a glance (the cycle
|
|
4096
|
+
// exit criterion is prod-only).
|
|
4097
|
+
console.log(` Manual Interventions: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
|
|
4098
|
+
if (inst.env_tag) {
|
|
4099
|
+
console.log(` Env Tag: ${inst.env_tag}`);
|
|
4100
|
+
}
|
|
3988
4101
|
if (inst.phases?.length) {
|
|
3989
4102
|
for (const phase of inst.phases) {
|
|
3990
4103
|
console.log(chalk.cyan(`\n Phase: ${phase.name}`));
|
|
@@ -4721,7 +4834,8 @@ program
|
|
|
4721
4834
|
.argument('<source-id>', 'Action, commitment, or pipeline step ID')
|
|
4722
4835
|
.requiredOption('--agent <name>', 'Agent name or member ID')
|
|
4723
4836
|
.option('--source-type <type>', 'Source type (action, commitment, pipeline_step)', 'action')
|
|
4724
|
-
.option('--prompt <prompt>', '
|
|
4837
|
+
.option('--prompt <prompt>', 'Operator instructions for this execution. Appended to the agent\'s built prompt as a labeled "## Operator Instructions" section. Takes precedence over runtime auto-completion instructions per the precedence rule (see architecture doc).')
|
|
4838
|
+
.option('--no-auto-complete', 'Suppress the runtime auto-completion instruction. The agent will not be told to mark the source done. Use this for "draft, don\'t close" workflows where a human reviews and locks. Default: off. Pipeline-step source: ignored with a warning.')
|
|
4725
4839
|
.option('--repos <repos>', 'Comma-separated repo names to clone (overrides agent config)', (v) => v.split(','))
|
|
4726
4840
|
.option('--model <model>', 'Model override (e.g., opus, sonnet)')
|
|
4727
4841
|
.option('--budget <usd>', 'Max budget override in USD', parseNumber)
|
|
@@ -4732,6 +4846,22 @@ program
|
|
|
4732
4846
|
console.error(chalk.red(`Invalid source type. Must be one of: ${validTypes.join(', ')}`));
|
|
4733
4847
|
process.exit(1);
|
|
4734
4848
|
}
|
|
4849
|
+
// [F24 §3.3 / EC3 / rul_no_auto_complete_pipeline_step_ignored]
|
|
4850
|
+
// --no-auto-complete is ignored for pipeline_step sources to preserve
|
|
4851
|
+
// back-compat for shipped pipeline executions. Layer-1 defense:
|
|
4852
|
+
// CLI prints a warning and does NOT forward the flag onto
|
|
4853
|
+
// context.noAutoComplete. Layer-2 defense lives in the agent's
|
|
4854
|
+
// buildSelfReportingInstructions, which gates the noAutoComplete
|
|
4855
|
+
// check on source_type !== 'pipeline_step' regardless.
|
|
4856
|
+
//
|
|
4857
|
+
// commander.js: `--no-auto-complete` produces opts.autoComplete=false
|
|
4858
|
+
// (NOT opts.noAutoComplete=true) because of the `--no-` prefix
|
|
4859
|
+
// convention. We surface that as the boolean `forwardSuppress` below.
|
|
4860
|
+
const operatorOptedOut = opts.autoComplete === false;
|
|
4861
|
+
if (operatorOptedOut && opts.sourceType === 'pipeline_step') {
|
|
4862
|
+
console.error(chalk.yellow('Warning: --no-auto-complete is ignored for pipeline_step sources; pipeline steps always auto-complete.'));
|
|
4863
|
+
}
|
|
4864
|
+
const forwardSuppress = operatorOptedOut && opts.sourceType !== 'pipeline_step';
|
|
4735
4865
|
// Resolve agent name to member ID if not already a UUID
|
|
4736
4866
|
let agentMemberId = opts.agent;
|
|
4737
4867
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(agentMemberId)) {
|
|
@@ -4768,8 +4898,24 @@ program
|
|
|
4768
4898
|
url: `https://github.com/formigio/${r}.git`,
|
|
4769
4899
|
branch: 'main',
|
|
4770
4900
|
})),
|
|
4771
|
-
cwd: '/workspace',
|
|
4772
4901
|
};
|
|
4902
|
+
// [F24 §3.2 / D8 / rul_workspace_clone_contract] cwd defaulting per
|
|
4903
|
+
// resolved repo count. Pre-F24 the CLI hardcoded cwd='/workspace',
|
|
4904
|
+
// which is ALWAYS empty inside the runner (clones land at
|
|
4905
|
+
// /home/agent/development/<repo>). BUG20's symptom ("'.git' missing")
|
|
4906
|
+
// was just the cwd misalignment — the clone itself was already
|
|
4907
|
+
// correct. Fix:
|
|
4908
|
+
// single repo → /home/agent/development/<repo> (align with clone)
|
|
4909
|
+
// multi repo → unset, let runner pick first repo via its existing
|
|
4910
|
+
// fallback at fazemos-agent-runner.ts:1075
|
|
4911
|
+
// zero repos → /workspace (preserve chat_session and no-repo cases)
|
|
4912
|
+
if (repoNames.length === 1) {
|
|
4913
|
+
context.cwd = `/home/agent/development/${repoNames[0]}`;
|
|
4914
|
+
}
|
|
4915
|
+
else if (repoNames.length === 0) {
|
|
4916
|
+
context.cwd = '/workspace';
|
|
4917
|
+
}
|
|
4918
|
+
// multi-repo: leave context.cwd unset — runner fallback chooses
|
|
4773
4919
|
// Resolve worksheet context for actions/commitments
|
|
4774
4920
|
if (opts.sourceType === 'action' || opts.sourceType === 'commitment') {
|
|
4775
4921
|
try {
|
|
@@ -4786,6 +4932,17 @@ program
|
|
|
4786
4932
|
context.worksheetName = detail.worksheet?.name;
|
|
4787
4933
|
context.worksheetPurpose = detail.worksheet?.purpose;
|
|
4788
4934
|
context.actionDescription = match.description || match.name;
|
|
4935
|
+
// [F24 §3.3 / D3 / rul_action_completion_semantics]
|
|
4936
|
+
// Propagate the action's target_value into context so the
|
|
4937
|
+
// agent's buildSelfReportingInstructions can branch:
|
|
4938
|
+
// target_value null|1 → binary auto-complete with -v 1
|
|
4939
|
+
// target_value > 1 → emit a "non-binary target" note,
|
|
4940
|
+
// NO auto-completion command
|
|
4941
|
+
// Only meaningful for source_type='action'; we set it only
|
|
4942
|
+
// here (the action-resolution branch).
|
|
4943
|
+
if (opts.sourceType === 'action') {
|
|
4944
|
+
context.actionTargetValue = match.target_value ?? null;
|
|
4945
|
+
}
|
|
4789
4946
|
// Include linked outcomes
|
|
4790
4947
|
if (detail.outcomes?.length) {
|
|
4791
4948
|
context.outcomes = detail.outcomes.map((o) => ({
|
|
@@ -4808,6 +4965,12 @@ program
|
|
|
4808
4965
|
}
|
|
4809
4966
|
if (opts.prompt)
|
|
4810
4967
|
context.prompt = opts.prompt;
|
|
4968
|
+
// [F24 §3.3 / D2] Forward --no-auto-complete intent. The CLI guard
|
|
4969
|
+
// above suppresses this for pipeline_step (forwardSuppress is false
|
|
4970
|
+
// in that case). Agent's buildSelfReportingInstructions reads
|
|
4971
|
+
// context.noAutoComplete; absent/false → default behavior.
|
|
4972
|
+
if (forwardSuppress)
|
|
4973
|
+
context.noAutoComplete = true;
|
|
4811
4974
|
const body = {
|
|
4812
4975
|
sourceType: opts.sourceType,
|
|
4813
4976
|
sourceId,
|
|
@@ -6901,6 +7064,156 @@ ops
|
|
|
6901
7064
|
process.exit(1);
|
|
6902
7065
|
}
|
|
6903
7066
|
});
|
|
7067
|
+
// ── OPS-2 — Manual Interventions KPI ─────────────────────────────────
|
|
7068
|
+
// Spec: specs/tech/platform/OPS-2-manual-intervention-kpi-tech-spec.md
|
|
7069
|
+
//
|
|
7070
|
+
// `fazemos ops interventions` has three operating modes:
|
|
7071
|
+
//
|
|
7072
|
+
// 1. bare — windowed summary table + current zero-intervention
|
|
7073
|
+
// streak. Calls GET /api/ops/manual-interventions/summary.
|
|
7074
|
+
// Prod-only by default (cycle exit criterion is prod);
|
|
7075
|
+
// --include-dev inverts the env_tag filter for parity
|
|
7076
|
+
// with the API.
|
|
7077
|
+
// 2. --pipeline — per-pipeline drill. Calls GET /api/pipeline-instances/:id
|
|
7078
|
+
// (response now carries manual_intervention_count +
|
|
7079
|
+
// env_tag) and surfaces the count + the env_tag fence so
|
|
7080
|
+
// operators can read the chip the same way the web does.
|
|
7081
|
+
// Forensic per-event detail lives behind `fazemos agents
|
|
7082
|
+
// events --type manual_intervention` (existing surface);
|
|
7083
|
+
// we print the hint so the operator doesn't have to
|
|
7084
|
+
// rediscover it.
|
|
7085
|
+
// 3. --since — windowed list. Calls GET /api/ops/manual-interventions
|
|
7086
|
+
// with the resolved `since` timestamp. Defaults to
|
|
7087
|
+
// include-dev (false) → prod-only-NO; the GET list
|
|
7088
|
+
// endpoint defaults exclude_dev=false (the broader view
|
|
7089
|
+
// is more useful when scanning recent activity). The
|
|
7090
|
+
// --exclude-dev flag tightens to prod-only.
|
|
7091
|
+
//
|
|
7092
|
+
// Admin/owner gating is enforced server-side (403 FORBIDDEN); the CLI
|
|
7093
|
+
// surfaces the API's error verbatim so non-admin operators see the
|
|
7094
|
+
// authoritative reason rather than a stale local guess.
|
|
7095
|
+
ops
|
|
7096
|
+
.command('interventions')
|
|
7097
|
+
.description('Manual-intervention KPI. Bare mode shows the windowed summary table + '
|
|
7098
|
+
+ 'current zero-intervention streak (prod-only by default). '
|
|
7099
|
+
+ '--pipeline <id> drills into one pipeline. '
|
|
7100
|
+
+ '--since <duration> lists pipelines started in the window.')
|
|
7101
|
+
.option('-w, --window <window>', 'Summary window: 24h, 7d, 30d', '7d')
|
|
7102
|
+
.option('-p, --pipeline <id>', 'Drill into one pipeline_instance by ID')
|
|
7103
|
+
.option('--since <duration>', 'List mode: filter by started_at >= now - duration (e.g. 24h, 7d, 30d)')
|
|
7104
|
+
.option('-l, --limit <n>', 'List mode: max rows (default 50, max 200)', '50')
|
|
7105
|
+
.option('--include-dev', 'Include dev-tagged pipelines in the summary / list (default: exclude)', false)
|
|
7106
|
+
.option('--exclude-dev', 'Force exclude dev-tagged pipelines (default for summary; opt-in for list)', false)
|
|
7107
|
+
.option('--json', 'Print the raw API response as JSON (machine-readable)')
|
|
7108
|
+
.action(async (opts) => {
|
|
7109
|
+
try {
|
|
7110
|
+
// ── Mode 1: --pipeline <id> drill ────────────────────────────
|
|
7111
|
+
if (opts.pipeline) {
|
|
7112
|
+
const data = await api('GET', `/api/pipeline-instances/${opts.pipeline}`);
|
|
7113
|
+
if (opts.json) {
|
|
7114
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
const inst = data.instance;
|
|
7118
|
+
if (!inst) {
|
|
7119
|
+
console.error(chalk.red(`Pipeline ${opts.pipeline} not found`));
|
|
7120
|
+
process.exit(1);
|
|
7121
|
+
}
|
|
7122
|
+
console.log(chalk.cyan(`Manual interventions on ${inst.name}`));
|
|
7123
|
+
console.log(` ID: ${inst.id}`);
|
|
7124
|
+
console.log(` Status: ${inst.status}`);
|
|
7125
|
+
console.log(` Env Tag: ${inst.env_tag ?? chalk.gray('—')}`);
|
|
7126
|
+
console.log(` Count: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
|
|
7127
|
+
if (inst.started_at) {
|
|
7128
|
+
console.log(` Started: ${new Date(inst.started_at).toLocaleString()}`);
|
|
7129
|
+
}
|
|
7130
|
+
if (inst.completed_at) {
|
|
7131
|
+
console.log(` Completed: ${new Date(inst.completed_at).toLocaleString()}`);
|
|
7132
|
+
}
|
|
7133
|
+
console.log('');
|
|
7134
|
+
console.log(chalk.gray(' Forensic per-event detail: query agent_events for the assignee/reviewer'));
|
|
7135
|
+
console.log(chalk.gray(' identities that drove each increment. Example:'));
|
|
7136
|
+
console.log(chalk.gray(` fazemos agents events <agent-id> --type manual_intervention`));
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
// ── Mode 2: --since <duration> windowed list ─────────────────
|
|
7140
|
+
if (opts.since) {
|
|
7141
|
+
const since = parseSinceDuration(opts.since);
|
|
7142
|
+
const params = [`since=${encodeURIComponent(since)}`];
|
|
7143
|
+
// For the list endpoint, the API default is exclude_dev=false (a
|
|
7144
|
+
// broader scan-recent-activity surface). --exclude-dev tightens
|
|
7145
|
+
// to prod-only. --include-dev is a no-op here (the default
|
|
7146
|
+
// already includes dev) but accepted so the flag is symmetric
|
|
7147
|
+
// across modes.
|
|
7148
|
+
if (opts.excludeDev)
|
|
7149
|
+
params.push('exclude_dev=true');
|
|
7150
|
+
const limitN = Math.max(1, Math.min(200, Number(opts.limit) || 50));
|
|
7151
|
+
params.push(`limit=${limitN}`);
|
|
7152
|
+
const qs = `?${params.join('&')}`;
|
|
7153
|
+
const data = await api('GET', `/api/ops/manual-interventions${qs}`);
|
|
7154
|
+
if (opts.json) {
|
|
7155
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7156
|
+
return;
|
|
7157
|
+
}
|
|
7158
|
+
const rows = data.pipelines || [];
|
|
7159
|
+
if (!rows.length) {
|
|
7160
|
+
console.log(chalk.green(`No interventions in window since ${since}`));
|
|
7161
|
+
return;
|
|
7162
|
+
}
|
|
7163
|
+
const scope = opts.excludeDev ? 'prod-only' : 'all envs';
|
|
7164
|
+
console.log(chalk.cyan(`Manual interventions since ${since} (${scope}):`));
|
|
7165
|
+
console.log('');
|
|
7166
|
+
// Columns: MI | Env | Status | Started | Name (ID)
|
|
7167
|
+
for (const r of rows) {
|
|
7168
|
+
const mi = formatManualInterventionCell(r.manual_intervention_count);
|
|
7169
|
+
const env = (r.env_tag ?? '—').padEnd(4);
|
|
7170
|
+
const status = (r.status ?? '').padEnd(10);
|
|
7171
|
+
const started = r.started_at ? new Date(r.started_at).toLocaleString() : '';
|
|
7172
|
+
console.log(` MI:${mi} ${chalk.gray(env)} ${status} ${chalk.gray(started)}`);
|
|
7173
|
+
console.log(` ${chalk.cyan(r.name)} ${chalk.gray(r.pipeline_instance_id)}`);
|
|
7174
|
+
}
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
// ── Mode 3: bare summary ─────────────────────────────────────
|
|
7178
|
+
// The summary endpoint defaults exclude_dev=true (cycle exit
|
|
7179
|
+
// criterion is prod-only by spec). --include-dev inverts. The CLI
|
|
7180
|
+
// is explicit either way so log lines are unambiguous.
|
|
7181
|
+
const excludeDev = opts.includeDev ? false : true;
|
|
7182
|
+
const window = opts.window || '7d';
|
|
7183
|
+
const validWindows = ['24h', '7d', '30d'];
|
|
7184
|
+
if (!validWindows.includes(window)) {
|
|
7185
|
+
console.error(chalk.red(`Invalid --window "${window}". Must be one of: ${validWindows.join(', ')}`));
|
|
7186
|
+
process.exit(1);
|
|
7187
|
+
}
|
|
7188
|
+
const qs = `?window=${window}&exclude_dev=${excludeDev}`;
|
|
7189
|
+
const data = await api('GET', `/api/ops/manual-interventions/summary${qs}`);
|
|
7190
|
+
if (opts.json) {
|
|
7191
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
const scope = excludeDev ? 'prod-only' : 'all envs';
|
|
7195
|
+
console.log(chalk.cyan(`Manual interventions — ${data.window} (${scope})`));
|
|
7196
|
+
console.log('');
|
|
7197
|
+
console.log(` Total runs: ${data.total_runs}`);
|
|
7198
|
+
console.log(` Total interventions: ${data.total_interventions}`);
|
|
7199
|
+
console.log(` Avg per run: ${typeof data.avg_per_run === 'number' ? data.avg_per_run.toFixed(2) : data.avg_per_run}`);
|
|
7200
|
+
console.log(` p95 per run: ${data.p95_per_run}`);
|
|
7201
|
+
console.log(` Zero-intervention: ${data.zero_intervention_runs} of ${data.total_runs} runs`);
|
|
7202
|
+
// Streak gets the same green/amber/grey emphasis as the web chip:
|
|
7203
|
+
// ≥5 is a celebration moment per Sage's spec; 1-4 amber; 0 grey.
|
|
7204
|
+
const streak = data.zero_intervention_streak ?? 0;
|
|
7205
|
+
const streakDisplay = streak >= 5
|
|
7206
|
+
? chalk.green(`${streak} 🎉`)
|
|
7207
|
+
: streak >= 1
|
|
7208
|
+
? chalk.yellow(String(streak))
|
|
7209
|
+
: chalk.gray('0');
|
|
7210
|
+
console.log(` Current streak: ${streakDisplay}`);
|
|
7211
|
+
}
|
|
7212
|
+
catch (err) {
|
|
7213
|
+
console.error(chalk.red(err.message));
|
|
7214
|
+
process.exit(1);
|
|
7215
|
+
}
|
|
7216
|
+
});
|
|
6904
7217
|
// ── F18 — Project-scoped docs surface ────────────────────────────────
|
|
6905
7218
|
// Spec: specs/tech/platform/F18-project-scoped-docs-surface-tech-spec.md §8
|
|
6906
7219
|
//
|