Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
},
"metadata": {
"description": "AWOS plugins for AI-native development workflows",
"version": "2.4.3"
"version": "2.4.4"
},
"plugins": [
{
"name": "awos",
"source": "./plugins/awos",
"description": "Extends AWOS core with AI-powered delivery automation: audits a project's AI-readiness across quality dimensions (/awos:ai-readiness-audit) and sets up an end-to-end delivery flow — feature and optional bug-fix commands — tailored to the team (/awos:flow).",
"version": "2.4.3"
"version": "2.4.4"
}
]
}
2 changes: 1 addition & 1 deletion plugins/awos/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "awos",
"version": "2.4.3",
"version": "2.4.4",
"description": "Extends AWOS core with AI-powered delivery automation: audits a project's AI-readiness across quality dimensions (/awos:ai-readiness-audit) and sets up an end-to-end delivery flow — feature and optional bug-fix commands — tailored to the team (/awos:flow).",
"author": {
"name": "Provectus"
Expand Down
2 changes: 1 addition & 1 deletion plugins/awos/commands/flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow this process precisely.

## Step 1: Prerequisite Checks & Mode Detection

1. **Generator version.** This command's generator version is `2.4.3` (a literal constant, kept in sync with the plugin manifest — the lint suite enforces the match). It has two jobs: Step 6 stamps it into every generated artifact's footer marker, and the re-run detector (item 4) compares each artifact's footer `version=` against it to decide whether the templates have moved on. It drives no session check of its own — a command loaded from a stale marketplace cache needs no upfront halt, because the next re-run on the updated command sees the older footer version and regenerates every stage.
1. **Generator version.** This command's generator version is `2.4.4` (a literal constant, kept in sync with the plugin manifest — the lint suite enforces the match). It has two jobs: Step 6 stamps it into every generated artifact's footer marker, and the re-run detector (item 4) compares each artifact's footer `version=` against it to decide whether the templates have moved on. It drives no session check of its own — a command loaded from a stale marketplace cache needs no upfront halt, because the next re-run on the updated command sees the older footer version and regenerates every stage.
2. If `context/product/architecture.md` does not exist, stop and tell the user to run `/awos:architecture` first.
3. If `context/product/hired-agents.md` does not exist, recommend running `/awos:hire` first (the generated flow references the hired specialists), but let the user continue without it.
4. Detect the mode:
Expand Down
4 changes: 2 additions & 2 deletions plugins/awos/skills/ai-readiness-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,14 @@ node "${CLAUDE_SKILL_DIR}/dist/cli.js" patch-judgment "context/audits/YYYY-MM-DD
node "${CLAUDE_SKILL_DIR}/dist/cli.js" patch-report "context/audits/YYYY-MM-DD_HH-MM-SS" context/audits/YYYY-MM-DD_HH-MM-SS/report-blocks.json
```

- `headline` — the executive band. Transcribe values verbatim from the dimension checks (cite the `check_id`); never invent numbers. Row 1 of the headline (capability Points + Coverage cap-score block) is emitted by the renderer directly from `audit_total`/`coverage` — do not add it as a `delivery[]` entry, and the two connector-gated rows (Cycle time In-Progress→Done, MTTR) are computed by the engine from the tracker artifact and appended by the renderer — do not author them either (an authored gated row is ignored). `delivery[]` carries only rows 2–7, each a `DeliveryMetric` object `{label, display_value?, band?, check_id?}`. Author them in this order, reading DORA bands from each check's `hint` field ("DORA-banded (high)"), and transcribing all values verbatim — never invent numbers:
- `headline` — the executive band. Transcribe values verbatim from the dimension checks (cite the `check_id`); never invent numbers. Row 1 of the headline (capability Points + Coverage cap-score block) is emitted by the renderer directly from `audit_total`/`coverage` — do not add it as a `delivery[]` entry, and the two connector-gated rows (Cycle time In-Progress→Done from the tracker artifact, MTTR from the incidents artifact) are computed by the engine and appended by the renderer — do not author them either (an authored gated row is ignored). `delivery[]` carries only rows 2–7, each a `DeliveryMetric` object `{label, display_value?, band?, check_id?}`. Author them in this order, reading DORA bands from each check's `hint` field ("DORA-banded (high)"), and transcribing all values verbatim — never invent numbers:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace the nonexistent Step 6.4 reference.

Both changed prompt documents refer to SKILL.md Step 6.4, but the current workflow places report authoring in Step 5, item 4. This can direct the orchestrator to the wrong section.

  • plugins/awos/skills/ai-readiness-audit/SKILL.md#L170-L170: change SKILL.md Step 6.4 to SKILL.md Step 5.4.
  • plugins/awos/skills/ai-readiness-audit/output-format.md#L77-L77: change SKILL.md Step 6.4 to SKILL.md Step 5.4.

As per coding guidelines, “Treat framework and plugin markdown files as prompts: prioritize clarity, structure, and explicit role, task, and process sections.”

📍 Affects 2 files
  • plugins/awos/skills/ai-readiness-audit/SKILL.md#L170-L170 (this comment)
  • plugins/awos/skills/ai-readiness-audit/output-format.md#L77-L77
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/awos/skills/ai-readiness-audit/SKILL.md` at line 170, Update the
nonexistent “SKILL.md Step 6.4” reference to “SKILL.md Step 5.4” in
plugins/awos/skills/ai-readiness-audit/SKILL.md at line 170 and
plugins/awos/skills/ai-readiness-audit/output-format.md at line 77, ensuring
both prompt documents point to the report-authoring workflow in Step 5, item 4.

Source: Coding guidelines

1. **Merges** — put the unit in the value, not the label: `label: "Merges"`, `display_value` from `report-context` → `window_stats.merges_per_active_per_week` rendered as a per-week rate `"<n> / week (per active contributor)"` (e.g. `"1.5 / week (per active contributor)"`); no `band`; no `check_id`; source: git artifact. If the value is null (zero active contributors), omit `display_value`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the Merges display format consistent.

Line 171 requires "<n> / week (per active contributor)", but output-format.md Line 85 still shows "3.2 / active contributor". Update one document so both prompts define the same format.

As per coding guidelines, “Treat framework and plugin markdown files as prompts: prioritize clarity, structure, and explicit role, task, and process sections.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/awos/skills/ai-readiness-audit/SKILL.md` at line 171, Align the
Merges display format in the relevant documentation so it consistently uses "<n>
/ week (per active contributor)" everywhere, including the output-format
guidance and the SKILL.md prompt. Preserve the existing null-value omission
behavior and all other metric requirements.

Source: Coding guidelines

2. **LOC** — `label: "LOC"`, `display_value` from `report-context` → `window_stats.loc_per_active_per_week` as a per-week rate `"<n> / week (per active contributor)"`; same rules.
3. **Deployment frequency** — check `DF-01`; band from hint; `check_id: "DF-01"`.
4. **Rework rate (DORA)** — check `DF-06`; band from hint; `check_id: "DF-06"`.
5. **Lead time for change** — check `DF-02`; band from hint; `check_id: "DF-02"`.
6. **Change-failure rate** — check `DF-04`; band from hint; `check_id: "DF-04"`.
(Rows 8–9 — **Cycle time (In-Progress→Done)** and **MTTR** — are engine-derived: `audit-core`/`enrich` compute the median and the honest gated note from `collected/tracker.json` into `audit.derived_delivery`, and the renderer appends them. Your job for cycle time is upstream, in step 1: fetch the tracker's per-ticket status history so `in_progress_at` is populated — the engine does the rest.)
(Rows 8–9 — **Cycle time (In-Progress→Done)** and **MTTR** — are engine-derived: `audit-core`/`enrich` compute the median and the honest gated note into `audit.derived_delivery` (cycle time from `collected/tracker.json`, MTTR from `collected/incidents.json`), and the renderer appends them. Your job is upstream, in step 1: for cycle time, fetch the tracker's per-ticket status history so `in_progress_at` is populated; for MTTR, write `collected/incidents.json` per `references/connector-shapes.md` — the engine does the rest.)

`scale[]` = code size & complexity — author exactly these three rows, each a `{label, display_value, check_id}` transcribed from its check (do not put commits, contributors, or merges here — those are activity, not scale/complexity):
1. **Source size** — `check_id: "DESC-04"` — LOC + file count (e.g. `"234k LOC · 1,203 files"`).
Expand Down
7 changes: 7 additions & 0 deletions plugins/awos/skills/ai-readiness-audit/artifact_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ export interface DerivedDelivery {
note?: string;
};
mttr: {
/** Median recovery time, e.g. "2 h". Absent when not measurable. */
display_value?: string;
median_hours?: number;
incidents_used?: number;
/** Honest state when the row stays empty (e.g. "PagerDuty connected — no incident with a resolved recovery span"). */
note?: string;
};
}
Expand Down Expand Up @@ -481,6 +486,7 @@ export const COLLECTOR_SOURCES = [
'tracker',
'docs',
'code_host',
'incidents',
] as const;

export const SOURCE_LABEL_DEFAULTS: Record<string, string> = {
Expand All @@ -492,5 +498,6 @@ export const SOURCE_LABEL_DEFAULTS: Record<string, string> = {
scale: 'source code (AST)',
audit: 'source code',
incident: 'incident source',
incidents: 'incident source',
'org-rollup': 'portfolio',
};
67 changes: 60 additions & 7 deletions plugins/awos/skills/ai-readiness-audit/audit_core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ import {
import { collect as collectCi } from './collectors/ci.ts';
import { collect as collectTracker } from './collectors/tracker.ts';
import { collect as collectDocs } from './collectors/docs.ts';
import {
collect as collectIncidents,
deriveIncidentAggregates,
hasMeasurableIncidents,
} from './collectors/incidents.ts';

/**
* Compute the connector-gated headline rows (Cycle time, MTTR) from the
Expand Down Expand Up @@ -94,6 +99,40 @@ export function computeDerivedDelivery(
}
}
const out: DerivedDelivery = { cycle_time: {}, mttr: {} };

// MTTR headline — derived from the incidents artifact, mirroring the metric,
// so the headline can never contradict the DF-07 detail below it. A tracker
// that merely names an incident system is not incident data and plays no part.
// Computed independently of the tracker, so it fills even with no tracker.
try {
const inc = JSON.parse(
readFileSync(join(collectedDir, 'incidents.json'), 'utf8')
);
if (inc?.available) {
const incRaw = (inc.raw ?? {}) as {
incidents?: Parameters<typeof deriveIncidentAggregates>[0];
source_label?: string | null;
};
const agg = deriveIncidentAggregates(
incRaw.incidents,
inc.period?.lookback_days
);
const incLabel = incRaw.source_label ?? 'incident source';
if (agg.resolved_count > 0 && agg.median_duration_hours !== null) {
out.mttr.median_hours = round1(agg.median_duration_hours);
out.mttr.incidents_used = agg.resolved_count;
out.mttr.display_value = `${out.mttr.median_hours} h`;
} else if (agg.count > 0) {
out.mttr.note = `${incLabel} connected — no incident with a resolved recovery span`;
} else {
out.mttr.note = `${incLabel} connected — no incidents in window`;
}
}
} catch {
// No incidents artifact — leave out.mttr empty; the renderer shows the
// "needs incident connector" gated fallback.
}

if (!tracker?.available) return out;

const raw = (tracker.raw ?? {}) as Record<string, unknown>;
Expand Down Expand Up @@ -131,10 +170,6 @@ export function computeDerivedDelivery(
out.cycle_time.note = `${label} connected — no tickets resolved in window`;
}

const incident = raw.incident_source;
if (typeof incident === 'string' && incident) {
out.mttr.note = `incident source "${incident}" declared — no incident data mapped`;
}
return out;
}

Expand Down Expand Up @@ -510,6 +545,10 @@ export async function auditCore(
collectCi(repoPath, period),
collectTracker(repoPath, period),
collectDocs(repoPath, period),
// Connector-passed: no connector in the base pass → available:false. The
// orchestrator overwrites collected/incidents.json with real data and
// `enrich` re-scores. Present so the SKIP artifact exists every run.
collectIncidents(repoPath, period),
]) {
writeArtifact(art as { source: string }, collectedDir);
}
Expand Down Expand Up @@ -597,11 +636,25 @@ export async function auditCore(
const trackerArt = readCollected('tracker');
const docsArt = readCollected('docs');
const codeHostArt = readCollected('code_host');
const incidentsArt = readCollected('incidents');
const incidentsRaw = incidentsArt?.raw as
| { incidents?: Parameters<typeof deriveIncidentAggregates>[0] }
| undefined;
const topology: TopologyFlags = computeTopology(repoPath, {
has_tracker: Boolean(trackerArt?.available),
has_docs_connector: Boolean(docsArt?.available),
// A real, measurable incident source: the connector-passed incidents
// artifact with at least one resolved recovery span in the window (derived
// from raw.incidents[], same as the metric). Merely naming an incident
// system in the tracker does NOT award the category — that would grant
// DF-07 (category 1103) full weight on a git proxy with zero incident data.
// Without a measurable span category 1103 stays SKIP.
has_incident_source: Boolean(
trackerArt?.available && trackerArt?.incident_source
incidentsArt?.available &&
hasMeasurableIncidents(
incidentsRaw?.incidents,
incidentsArt?.period?.lookback_days
)
),
Comment on lines 644 to 658

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .raw fix is right — the old top-level read meant this branch never fired in any audit to date — but making it live changes scoring in a way the PR's own docs disavow: a tracker that merely names an incident system now flips has_incident_source, and the git-proxy fallback still calls awardCategories, so 1103's full weight 5 is granted on a not-reliable proxy with zero incident data. That contradicts the new standards.toml definition ("awarded when at least one incident has a resolved recovery span"), the DF-07 What-line, and the comment two lines up. There's also no test pinning either leg of this expression — reverting the .raw fix fails nothing. I'd either drop the tracker disjunct (award only on measured spans) or keep it deliberately and align the three doc strings; either way a couple of pinning tests would guard it (tracker-declared path → flag true; incidents available with zero resolved → flag false).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the tracker disjunct — 1103 is awarded only on a measured recovery span. standards.toml sources are incidents-only now, and I added the pinning tests: tracker-declared-only leaves DF-07 SKIP (e2e), plus hasMeasurableIncidents true/false legs (unit).

has_code_host: Boolean(codeHostArt?.available),
});
Expand Down Expand Up @@ -1034,7 +1087,7 @@ const CONNECTABLE_SOURCES = new Set([
'tracker',
'docs',
'ci',
'incident',
'incidents',
'code_host',
]);

Expand All @@ -1045,7 +1098,7 @@ const COLLECTED_ARTIFACT_SOURCES = new Set([
'ci',
'tracker',
'docs',
'incident',
'incidents',
'code_host',
]);

Expand Down
188 changes: 188 additions & 0 deletions plugins/awos/skills/ai-readiness-audit/collectors/incidents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* incidents — external incident source collector (connector-passed).
*
* Mirrors the tracker/ci/docs collectors: it does not reach out to a service
* itself. The orchestrator fetches incidents from whatever real incident source
* the project uses — a dedicated tool (PagerDuty / OpsGenie / incident.io), a
* status page (Statuspage / Atlassian), or code-host issues labelled as
* incidents (GitHub/GitLab) — normalises them to `IncidentRecord`s, and passes
* them here (see references/connector-shapes.md). Without a connector the
* artifact is `available: false`, so the MTTR metric falls back to its git
* proxy and category 1103 stays SKIP.
*
* The measured signal is real recovery time: for each resolved incident,
* resolved_at − started_at. The engine — not the orchestrator — derives the
* median: `deriveIncidentAggregates` is the one place that computes it, shared
* by this collector, the MTTR metric, and the topology gate. That mirrors the
* tracker collector, whose metrics derive their aggregates from `raw.tickets[]`
* rather than trusting a hand-written count, so an orchestrator arithmetic slip
* can never become a "measured" DORA number.
*/
import { makeArtifact, type Period } from './_base.ts';

/** One incident, normalised from whatever source the orchestrator fetched. */
export interface IncidentRecord {
id: string;
/** ISO 8601 — when the incident was opened/detected. */
started_at: string;
/** ISO 8601 — when service was restored; omit/null for still-open incidents. */
resolved_at?: string | null;
/** Optional severity label, verbatim from the source (e.g. "SEV1", "critical"). */
severity?: string | null;
/** Which source this came from, e.g. "pagerduty", "github-label:incident". */
source?: string | null;
}

export interface IncidentsConnector {
incidents?: IncidentRecord[];
/** Human label of the source for the report, e.g. "PagerDuty", "GitHub incident labels". */
source_label?: string | null;
}

export interface IncidentsRaw {
incidents: IncidentRecord[];
count: number;
/** Incidents with a valid started→resolved span (the only ones MTTR can measure). */
resolved_count: number;
/** Resolved incidents whose span could not be parsed (bad/zero/reversed timestamps). */
invalid_count: number;
/** Median recovery time in hours over resolved incidents; null when none. */
median_duration_hours: number | null;
source_label: string | null;
}

export function median(nums: number[]): number | null {
if (nums.length === 0) return null;
const s = [...nums].sort((a, b) => a - b);
const mid = Math.floor(s.length / 2);
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
}

/**
* Recovery time in hours for a resolved incident, or null when unmeasurable.
* A record with no `resolved_at` is still open (not invalid). A record WITH a
* `resolved_at` but an unparseable, reversed, or zero-length span is invalid —
* `end <= start` is rejected so an auto-resolved alert flap (a 0h span) cannot
* band "elite".
*/
export function durationHours(inc: IncidentRecord): number | null {
if (inc.resolved_at == null || inc.resolved_at === '') return null;
const start = Date.parse(inc.started_at ?? '');
const end = Date.parse(inc.resolved_at);
if (Number.isNaN(start) || Number.isNaN(end) || end <= start) return null;
return (end - start) / 3_600_000;
}

/**
* Clamp incidents to the audit window, anchored to the NEWEST `started_at` —
* mirroring `clampToWindow` for CI runs and tracker tickets. Connectors
* routinely over-fetch (all-time incident history); without this clamp that
* history would shift the "audit-window" median and make it non-reproducible
* across differently-scoped fetches. Records with no parseable `started_at` are
* kept (they cannot be judged against the window). No lookback → no clamp.
*/
export function clampIncidentsToWindow(
incidents: IncidentRecord[],
lookbackDays?: number | null
): { kept: IncidentRecord[]; dropped: number } {
if (!lookbackDays || lookbackDays <= 0)
return { kept: incidents, dropped: 0 };
let anchor = -Infinity;
const stamps = incidents.map((inc) => {
const t = Date.parse(inc.started_at ?? '');
if (Number.isFinite(t) && t > anchor) anchor = t;
return t;
});
if (!Number.isFinite(anchor)) return { kept: incidents, dropped: 0 };
const since = anchor - lookbackDays * 86_400_000;
const kept: IncidentRecord[] = [];
let dropped = 0;
incidents.forEach((inc, i) => {
if (Number.isFinite(stamps[i]) && stamps[i] < since) dropped++;
else kept.push(inc);
});
return { kept, dropped };
}

export interface IncidentAggregates extends IncidentsRaw {
/** In-window incidents kept after the window clamp. */
incidents: IncidentRecord[];
/** Incidents dropped for falling outside the audit window. */
dropped_out_of_window: number;
}

/**
* The single deterministic derivation of the MTTR aggregates from raw incident
* records. Clamps to the window, then over the in-window incidents: counts
* resolved-with-a-measurable-span, counts resolved-but-unmeasurable
* (`invalid_count`), and takes the median span. Shared by the collector, the
* MTTR metric, and the topology gate so the awarded category and the reported
* value can never diverge.
*/
export function deriveIncidentAggregates(
incidents: IncidentRecord[] | undefined | null,
lookbackDays?: number | null
): IncidentAggregates {
const all = Array.isArray(incidents) ? incidents : [];
const { kept, dropped } = clampIncidentsToWindow(all, lookbackDays);
const durations: number[] = [];
let invalid = 0;
for (const inc of kept) {
if (inc.resolved_at == null || inc.resolved_at === '') continue; // still open
const d = durationHours(inc);
if (d === null) invalid++;
else durations.push(d);
}
return {
incidents: kept,
count: kept.length,
resolved_count: durations.length,
invalid_count: invalid,
median_duration_hours: median(durations),
source_label: null,
dropped_out_of_window: dropped,
};
}

/**
* Whether an incidents artifact carries at least one measurable recovery span
* within the window — the predicate that gates category 1103. Shared by the
* topology gate and the MTTR metric so "awarded" and "measured" stay in lock
* step.
*/
export function hasMeasurableIncidents(
incidents: IncidentRecord[] | undefined | null,
lookbackDays?: number | null
): boolean {
return deriveIncidentAggregates(incidents, lookbackDays).resolved_count > 0;
}

export function collect(
_repoPath: string,
period: Period,
connector?: IncidentsConnector
) {
if (connector === undefined || connector === null) {
return makeArtifact(
'incidents',
false,
'no incident source connected; supply a PagerDuty/OpsGenie/incident.io, Statuspage, or code-host incident-label connector to measure MTTR from real incidents',
period,
{} as IncidentsRaw
);
}

const agg = deriveIncidentAggregates(
connector.incidents,
period.lookback_days
);
const raw: IncidentsRaw = {
incidents: agg.incidents,
count: agg.count,
resolved_count: agg.resolved_count,
invalid_count: agg.invalid_count,
median_duration_hours: agg.median_duration_hours,
source_label: connector.source_label ?? null,
};
return makeArtifact('incidents', true, null, period, raw);
}
Loading