Skip to content
Merged
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
2 changes: 2 additions & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ analysis artifacts.
| **ticket-007** | [`README.md`](./ticket-007/README.md) | [`preprompt.md`](./ticket-007/preprompt.md) | - | [`ai-codex.md`](./ticket-007/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-007/ai-codex-logs.txt) | [`changelog.md`](./ticket-007/changelog.md) |
| **ticket-008** | [`README.md`](./ticket-008/README.md) | [`preprompt.md`](./ticket-008/preprompt.md) | - | [`ai-codex.md`](./ticket-008/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-008/ai-codex-logs.txt) | [`changelog.md`](./ticket-008/changelog.md) |
| **ticket-009** | [`README.md`](./ticket-009/README.md) | [`preprompt.md`](./ticket-009/preprompt.md) | - | [`ai-codex.md`](./ticket-009/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-009/ai-codex-logs.txt) | [`changelog.md`](./ticket-009/changelog.md) |
| **ticket-010** | [`README.md`](./ticket-010/README.md) | [`preprompt.md`](./ticket-010/preprompt.md) | - | [`ai-codex.md`](./ticket-010/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-010/ai-codex-logs.txt) | [`changelog.md`](./ticket-010/changelog.md) |
| **ticket-011** | [`README.md`](./ticket-011/README.md) | [`preprompt.md`](./ticket-011/preprompt.md) | - | [`ai-codex.md`](./ticket-011/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-011/ai-codex-logs.txt) | [`changelog.md`](./ticket-011/changelog.md) |
<!-- AUTO:TICKET_INDEX:END -->
40 changes: 40 additions & 0 deletions project/ticket-011/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Ticket 011: Implement energy telemetry and URI Process opportunity ranking

- **ID**: ticket-011
- **Owner**: unresolved:human
- **Status**: IN_PROGRESS
- **Workflow state**: VALIDATION
- **Created**: 2026-08-30

## Goal and scope

Implement the v2 measurement contract declared by ticket-010. Observe optional
RAPL, cgroup v2 and PSI counters without controlling the process. Rank recurring
Process URIs independently by CPU, wall time, energy or I/O savings potential.
Accept an explicit process revision for version-over-version regression analysis.

## Acceptance criteria

- [x] AC-01: Scope is approved by the originating user request.
- [x] AC-02: Existing v1 samples remain readable and v2 telemetry discloses
method, attribution and confidence.
- [x] AC-03: Ranking requires twelve comparable samples by default and never
combines unlike resource units into an implicit score.
- [x] AC-04: CLI tests cover the opportunity query.
- [x] AC-05: Functional tests, a v2 schema instance check, a live
smoke test and governance pass.

## Validation evidence

- `/usr/bin/python3 -m pytest -q`: 14 passed.
- A generated v2 sample validates with Draft 2020-12 JSON Schema.
- A live `sleep` measurement and CPU opportunity query completed successfully.
- Runtime and CLI versions are regression-tested against package metadata.
- The live host exposed shared cgroup/PSI evidence but no readable RAPL domain;
energy was correctly marked unavailable.

## Participants

- Human participant: user:tom; authorization is recorded by the originating
request and no synthesized user file was created.
- Agent participant: [ai-codex.md](ai-codex.md)
Empty file.
45 changes: 45 additions & 0 deletions project/ticket-011/ai-codex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
participant-id: agent:codex
participant: codex
role: agent
ticket: ticket-011
---
# Participant: codex (AI agent)

## Understanding

Implement the application half of ticket-010 without claiming exclusive energy
or cgroup attribution on a shared host. Keep measurement advisory and private.

## Execution plan

1. Add backward-compatible v2 sample objects.
2. Observe optional RAPL/cgroup/PSI counters without privilege.
3. Add objective-specific ranking and CLI exposure.
4. Verify behavior and governance.

## Actual changes

- Corrected the runtime and CLI version projection to match package version
`0.2.0`, with a regression test covering both interfaces.

- Initialized the bounded ticket and recorded SESSION_EXECUTION_AUTHORIZATION
from the request to execute this work.
- Added backward-compatible sample v2 serialization with explicit unavailable
fallbacks for optional telemetry.
- Added read-only Linux RAPL, host CPU, cgroup v2 and PSI observation. Raw
cgroup paths are replaced by bounded hashes and shared attribution is named.
- Added CPU, wall, energy and I/O opportunity ranking with a twelve-sample
default gate, explicit units and an advisory-only result.
- Added CLI coverage and passed fourteen tests, schema validation, a live smoke
test and repository governance.
- Added a quiet measurement mode for runtimes that must preserve the wrapped
process output channel while appending resource evidence.

## Blockers

- None inside the recorded intent; proceed without a second confirmation.
- New authority remains required for destructive action, secret access, new
external coordination or material objective expansion. Protected delivery
may be invoked without another prompt when publication is in scope; its
exact-head trusted approval remains independent evidence.
6 changes: 6 additions & 0 deletions project/ticket-011/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Ticket Changelog (ticket-011)

## [0.1.0] - 2026-08-30

- Initial governance scaffold created.
- No human participant identity or content was generated.
62 changes: 62 additions & 0 deletions project/ticket-011/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-011",
"summary": "Implement energy telemetry and URI Process opportunity ranking",
"workstream": "application",
"classification": {
"kind": "FEATURE",
"priority": "P1",
"origin": "requested"
},
"placement": {
"home": "semcod",
"runtimeOwner": "semcod",
"shape": "runtime_service",
"adopt": ["wellmanifest/new-project", "wellmanifest/logs", "wellmanifest/performance"]
},
"allowedPaths": ["project/ticket-011/**", "TODO.md", "project/TICKETS.md", "src/**", "tests/**"],
"forbiddenPaths": ["project/ticket-*/user-*.md"],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "4fbc1477fee93d72f14b13164b5f85a308a359b2",
"targetBranch": "main",
"outcome": "Collect optional Linux energy and pressure evidence and rank recurring URI Process savings under an explicit objective.",
"nonGoals": [
"Create or mutate cgroups, scheduling policy or process authorization.",
"Claim process-exclusive attribution for shared counters.",
"Persist arguments, output, environment values, raw cgroup paths or secrets.",
"Apply optimizations automatically or rank from fewer than twelve comparable samples by default."
],
"complexity": "L",
"estimatedMinutes": 30,
"budgets": {
"maxImplementationFiles": 15,
"maxAffectedComponents": 5,
"maxPublicInterfaceChanges": 3,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Observe existing counters without privilege, retain v1 reads, disclose attribution confidence and rank CPU, wall, energy or I/O independently.",
"components": [
{"name": "sample-and-observer", "paths": ["src/estimation/model.py", "src/estimation/monitor.py"]},
{"name": "analysis-interface", "paths": ["src/estimation/__init__.py", "src/estimation/stats.py", "src/estimation/cli.py"]},
{"name": "verification", "paths": ["tests/**"]}
],
"responsibilityChanges": false,
"interfaceChanges": ["Emit v2 samples with optional telemetry.", "Add the opportunities CLI query.", "Accept an explicit bounded process revision."],
"dataChanges": [],
"ui": {"impact": "none", "states": [], "evidence": []},
"rollback": "Stop emitting v2 and remove the query; existing append-only evidence remains readable."
},
"runtimeDependencies": [],
"validation": [
{"criterion": "AC-02", "commands": ["PYTHONPATH=src python -m pytest -q"], "evidence": "bounded optional telemetry and v1 compatibility"},
{"criterion": "AC-03", "commands": ["PYTHONPATH=src python -m pytest -q"], "evidence": "objective-specific rankings and minimum sample gate"},
{"criterion": "AC-05", "commands": ["PYTHONPATH=src ./project/governance-check.sh"], "evidence": "repository governance passes"}
]
}
}
15 changes: 15 additions & 0 deletions project/ticket-011/preprompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Ticket preprompt

- **Task ID**: ticket-011
- **Task title**: Implement energy telemetry and URI Process opportunity ranking
- **Created**: 2026-08-30T10:39:06Z

Keep executable implementation outside this governance/evidence directory.
Read a human-owned user-*.md file only when one exists.
The request to execute this work creates SESSION_EXECUTION_AUTHORIZATION;
proceed within the recorded intent without a redundant confirmation prompt.
Require new authority for destructive action, secrets, external coordination,
or material objective expansion. When publication is in scope, session
authorization permits the declared protected delivery process and its merge
after exact-head trusted approval without another prompt. Session prose is
never approval evidence and the agent must not merge directly.
4 changes: 2 additions & 2 deletions src/estimation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Resource estimation for ticket-bound URI processes."""

__version__ = "0.2.0"

from estimation.model import Sample, canonical_process_uri
from estimation.monitor import measure_command, observe_pid
from estimation.stats import aggregate_samples, estimate_workload
Expand All @@ -15,5 +17,3 @@
"measure_command",
"observe_pid",
]

__version__ = "0.1.0"
27 changes: 24 additions & 3 deletions src/estimation/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import sys
from typing import Sequence

from estimation import __version__
from estimation.model import Sample
from estimation.monitor import measure_command, observe_pid
from estimation.stats import aggregate_samples, estimate_workload
from estimation.stats import aggregate_samples, estimate_workload, rank_opportunities
from estimation.store import append_sample, load_samples


Expand All @@ -25,15 +26,17 @@ def _context(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--store", default=DEFAULT_STORE)
parser.add_argument("--events", default=DEFAULT_EVENTS)
parser.add_argument("--interval", type=float, default=1.0)
parser.add_argument("--process-revision", default=os.getenv("SUBACTOR_PROCESS_REVISION"))


def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="estimation")
parser.add_argument("--version", action="version", version="estimation 0.1.0")
parser.add_argument("--version", action="version", version=f"estimation {__version__}")
commands = parser.add_subparsers(dest="command_name", required=True)

run = commands.add_parser("run", help="measure a caller-authorized command")
_context(run)
run.add_argument("--quiet", action="store_true", help="persist the sample without printing it")
run.add_argument("command", nargs=argparse.REMAINDER)

observe = commands.add_parser("observe", help="observe an existing PID tree")
Expand All @@ -50,6 +53,12 @@ def _parser() -> argparse.ArgumentParser:
estimate.add_argument("--quantity", type=int, default=1)
estimate.add_argument("--parallelism", type=int, default=1)

opportunities = commands.add_parser("opportunities", help="rank recurring optimization opportunities")
opportunities.add_argument("--store", default=DEFAULT_STORE)
opportunities.add_argument("--objective", choices=("cpu", "wall", "energy", "io"), default="cpu")
opportunities.add_argument("--minimum-samples", type=int, default=12)
opportunities.add_argument("--reduction-fraction", type=float, default=0.30)

validate = commands.add_parser("validate", help="validate all stored samples")
validate.add_argument("--store", default=DEFAULT_STORE)
return parser
Expand All @@ -63,7 +72,8 @@ def _require_process_uri(value: str | None) -> str:

def _write_sample(sample: Sample, args: argparse.Namespace) -> None:
append_sample(sample, Path(args.store), Path(args.events))
print(json.dumps(sample.to_dict(), ensure_ascii=False, sort_keys=True))
if not getattr(args, "quiet", False):
print(json.dumps(sample.to_dict(), ensure_ascii=False, sort_keys=True))


def main(argv: Sequence[str] | None = None) -> int:
Expand All @@ -80,6 +90,7 @@ def main(argv: Sequence[str] | None = None) -> int:
ticket_id=args.ticket,
correlation_id=args.correlation_id,
interval_seconds=args.interval,
process_revision=args.process_revision,
)
_write_sample(sample, args)
return int(sample.exit_code or 0)
Expand All @@ -92,6 +103,7 @@ def main(argv: Sequence[str] | None = None) -> int:
correlation_id=args.correlation_id,
interval_seconds=args.interval,
duration_seconds=args.duration,
process_revision=args.process_revision,
)
_write_sample(sample, args)
return 0
Expand All @@ -108,6 +120,15 @@ def main(argv: Sequence[str] | None = None) -> int:
)
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if args.command_name == "opportunities":
result = rank_opportunities(
samples,
objective=args.objective,
minimum_samples=args.minimum_samples,
reduction_fraction=args.reduction_fraction,
)
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if args.command_name == "validate":
print(json.dumps({"ok": True, "samples": len(samples), "store": str(args.store)}))
return 0
Expand Down
56 changes: 53 additions & 3 deletions src/estimation/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,43 @@
import json
import os
import platform
import re
from typing import Any, Sequence
from urllib.parse import urlsplit, urlunsplit
import uuid

import psutil


SAMPLE_SCHEMA = "semcod.estimation.sample/v1"
SAMPLE_SCHEMA = "semcod.estimation.sample/v2"
SUPPORTED_SAMPLE_SCHEMAS = {"semcod.estimation.sample/v1", SAMPLE_SCHEMA}
PROCESS_REVISION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,127}$")


def unavailable_energy() -> dict[str, Any]:
return {
"joules": None,
"method": "unavailable",
"confidence": "none",
"domains": 0,
}


def unavailable_kernel_observation() -> dict[str, Any]:
return {
"cgroup_id": None,
"attribution": "unavailable",
"cpu_seconds": None,
"memory_peak_bytes": None,
"read_bytes": None,
"write_bytes": None,
"pids_peak": None,
"pressure": {
"cpu_some_seconds": None,
"io_some_seconds": None,
"memory_some_seconds": None,
},
}


def utc_now() -> str:
Expand All @@ -31,6 +60,15 @@ def canonical_process_uri(value: str) -> str:
return canonical[:320]


def canonical_process_revision(value: str | None) -> str | None:
revision = str(value or "").strip()
if not revision:
return None
if not PROCESS_REVISION.fullmatch(revision):
raise ValueError("process revision contains unsupported characters")
return revision


def argv_sha256(argv: Sequence[str]) -> str:
encoded = json.dumps(list(argv), ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
Expand Down Expand Up @@ -74,15 +112,21 @@ class Sample:
host_profile: dict[str, Any]
raw_output_included: bool = False
secret_material_included: bool = False
energy: dict[str, Any] | None = None
kernel: dict[str, Any] | None = None
process_revision: str | None = None

def to_dict(self) -> dict[str, Any]:
return asdict(self)

@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "Sample":
if payload.get("schema") != SAMPLE_SCHEMA:
if payload.get("schema") not in SUPPORTED_SAMPLE_SCHEMAS:
raise ValueError("unsupported estimation sample schema")
return cls(**payload)
normalized = dict(payload)
normalized.setdefault("energy", unavailable_energy())
normalized.setdefault("kernel", unavailable_kernel_observation())
return cls(**normalized)


def build_sample(
Expand All @@ -105,6 +149,9 @@ def build_sample(
outcome: str,
program: str,
argv: Sequence[str],
energy: dict[str, Any] | None = None,
kernel: dict[str, Any] | None = None,
process_revision: str | None = None,
) -> Sample:
canonical = canonical_process_uri(process_uri)
duration = max(0.0, float(duration_seconds))
Expand Down Expand Up @@ -133,4 +180,7 @@ def build_sample(
program=os.path.basename(program)[:128] or "unknown",
argv_sha256=argv_sha256(argv),
host_profile=host_profile(),
energy=energy or unavailable_energy(),
kernel=kernel or unavailable_kernel_observation(),
process_revision=canonical_process_revision(process_revision),
)
Loading
Loading