Compare commits
5
Commits
v0.1.0
...
7aeeb6a6c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aeeb6a6c7 | ||
|
|
c873047a60 | ||
|
|
90b0a2ee88 | ||
|
|
b9ca192c51 | ||
|
|
2f56ce62ce |
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
@@ -24,7 +24,8 @@ jobs:
|
|||||||
|
|
||||||
- name: 📦 Install lint tools
|
- name: 📦 Install lint tools
|
||||||
run: |
|
run: |
|
||||||
pip install ruff
|
python -m pip install --upgrade pip
|
||||||
|
pip install --group lint
|
||||||
sudo apt-get update && sudo apt-get install -y shellcheck
|
sudo apt-get update && sudo apt-get install -y shellcheck
|
||||||
npm install -g markdownlint-cli2
|
npm install -g markdownlint-cli2
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ jobs:
|
|||||||
|
|
||||||
- name: 📦 Install lint tools
|
- name: 📦 Install lint tools
|
||||||
run: |
|
run: |
|
||||||
pip install ruff
|
python -m pip install --upgrade pip
|
||||||
|
pip install --group lint
|
||||||
sudo apt-get update && sudo apt-get install -y shellcheck
|
sudo apt-get update && sudo apt-get install -y shellcheck
|
||||||
npm install -g markdownlint-cli2
|
npm install -g markdownlint-cli2
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[dependency-groups]
|
||||||
|
lint = ["ruff==0.17.3"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
+1
-1
@@ -30,6 +30,6 @@ fi
|
|||||||
log "skills: updating from ${lockfile}..."
|
log "skills: updating from ${lockfile}..."
|
||||||
jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do
|
jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do
|
||||||
log "skills: updating ${source}..."
|
log "skills: updating ${source}..."
|
||||||
npx --yes skills add --yes "${source}"
|
npx --yes skills add --yes --agent claude-code "${source}" </dev/null
|
||||||
done
|
done
|
||||||
log "skills: update complete."
|
log "skills: update complete."
|
||||||
@@ -26,10 +26,16 @@ git log --no-merges --format='%h %s%n%b' <tag>..HEAD
|
|||||||
commit it reverts inside the range.
|
commit it reverts inside the range.
|
||||||
- A subject is a lead, not the change: read the diff (`git show --stat`, then
|
- A subject is a lead, not the change: read the diff (`git show --stat`, then
|
||||||
the hunks) when the subject does not say what changed for the user.
|
the hunks) when the subject does not say what changed for the user.
|
||||||
- When `gh` is available, check the previous tag's CI run
|
- When `gh` is available, read the CI of the previous tag's commit and of HEAD,
|
||||||
(`gh run list --branch <tag> -L 1`). A failed run means that version was
|
every workflow (`gh run list --commit <sha> --json
|
||||||
never published: say so under *Upgrade notes* and carry its upgrade notes
|
workflowName,status,conclusion`), not one picked by `-L 1`.
|
||||||
forward to this release.
|
- Previous tag: a workflow that finished as a failure means that version was
|
||||||
|
never published: say so under *Upgrade notes* and carry its upgrade notes
|
||||||
|
forward to this release.
|
||||||
|
- HEAD: a workflow that finished as a failure means pkgmgr would tag a
|
||||||
|
release that never goes stable, because its CI gate runs after the tag.
|
||||||
|
Queued, running or missing runs are normal; report only the failure, in
|
||||||
|
step 5.
|
||||||
|
|
||||||
## 2. Sort into sections
|
## 2. Sort into sections
|
||||||
|
|
||||||
@@ -130,6 +136,8 @@ used, shorten only the lines it names, and lint again.
|
|||||||
`feat` → minor, otherwise patch).
|
`feat` → minor, otherwise patch).
|
||||||
3. One line with the lint result (`markdownlint-cli2: 0 errors` or the manual
|
3. One line with the lint result (`markdownlint-cli2: 0 errors` or the manual
|
||||||
check that ran).
|
check that ran).
|
||||||
|
4. Only when a workflow on HEAD finished as a failure: one line naming it and
|
||||||
|
its run, and the advice not to release before it is green.
|
||||||
|
|
||||||
Then stop. Never edit CHANGELOG.md or a version file, never run
|
Then stop. Never edit CHANGELOG.md or a version file, never run
|
||||||
`pkgmgr release`, never tag: the operator releases.
|
`pkgmgr release`, never tag: the operator releases.
|
||||||
@@ -23,9 +23,13 @@ from pathlib import Path
|
|||||||
FAILED = {"failure", "cancelled", "timed_out"}
|
FAILED = {"failure", "cancelled", "timed_out"}
|
||||||
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
||||||
STAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?")
|
STAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?")
|
||||||
GENERIC = re.compile(r"Process completed with exit code|failed=0\b|FAILED - RETRYING")
|
GENERIC = re.compile(r"failed=0\b|FAILED - RETRYING|^make(\[\d+\])?: \*\*\* \[.*\] Error \d+")
|
||||||
|
PROCESS_END = re.compile(r"##\[error\]Process completed with exit code")
|
||||||
ANNOTATION = re.compile(r"##\[error\]")
|
ANNOTATION = re.compile(r"##\[error\]")
|
||||||
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
|
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
|
||||||
|
LINT_AT = re.compile(r"\S+:\d+:\d+:? ((error|warning) )?[A-Z]{1,4}\d{3,4}\b")
|
||||||
|
LINT_CODE = re.compile(r"^[A-Z]{1,4}\d{3,4}\b")
|
||||||
|
LINT_ARROW = re.compile(r"^-->\s+\S+:\d+:\d+")
|
||||||
SIGNAL = re.compile(r"\b(ERROR|Error|FAILED|FAIL|fatal|Exception|failed)\b")
|
SIGNAL = re.compile(r"\b(ERROR|Error|FAILED|FAIL|fatal|Exception|failed)\b")
|
||||||
TASK = re.compile(r"^TASK \[")
|
TASK = re.compile(r"^TASK \[")
|
||||||
REFUSAL = "terminal escape sequences"
|
REFUSAL = "terminal escape sequences"
|
||||||
@@ -49,6 +53,8 @@ def output_lines(text: str) -> list[tuple[str, str]]:
|
|||||||
kept, echo, task = [], False, ""
|
kept, echo, task = [], False, ""
|
||||||
for raw in text.splitlines():
|
for raw in text.splitlines():
|
||||||
line = clean(raw)
|
line = clean(raw)
|
||||||
|
if PROCESS_END.search(line):
|
||||||
|
break
|
||||||
if line.startswith("##[group]Run "):
|
if line.startswith("##[group]Run "):
|
||||||
echo = True
|
echo = True
|
||||||
elif line.startswith("##[endgroup]"):
|
elif line.startswith("##[endgroup]"):
|
||||||
@@ -60,13 +66,28 @@ def output_lines(text: str) -> list[tuple[str, str]]:
|
|||||||
return kept
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def is_lint(lines: list[tuple[str, str]], index: int) -> bool:
|
||||||
|
line = lines[index][1]
|
||||||
|
if LINT_AT.search(line):
|
||||||
|
return True
|
||||||
|
following = lines[index + 1][1] if index + 1 < len(lines) else ""
|
||||||
|
return bool(LINT_CODE.match(line) and LINT_ARROW.match(following))
|
||||||
|
|
||||||
|
|
||||||
def signature(text: str) -> str:
|
def signature(text: str) -> str:
|
||||||
lines = output_lines(text)
|
lines = output_lines(text)
|
||||||
for pattern in (ANNOTATION, FATAL, SIGNAL):
|
tiers = (
|
||||||
for task, line in reversed(lines):
|
(lambda i: ANNOTATION.search(lines[i][1]), False),
|
||||||
if pattern.search(line):
|
(lambda i: FATAL.search(lines[i][1]), True),
|
||||||
|
(lambda i: is_lint(lines, i), False),
|
||||||
|
(lambda i: SIGNAL.search(lines[i][1]), False),
|
||||||
|
)
|
||||||
|
for matches, with_task in tiers:
|
||||||
|
for index in reversed(range(len(lines))):
|
||||||
|
if matches(index):
|
||||||
|
task, line = lines[index]
|
||||||
found = ANNOTATION.sub("", line).strip()
|
found = ANNOTATION.sub("", line).strip()
|
||||||
return normalise(f"{task} {found}" if pattern is FATAL and task else found)
|
return normalise(f"{task} {found}" if with_task and task else found)
|
||||||
return normalise(lines[-1][1]) if lines else "<empty log>"
|
return normalise(lines[-1][1]) if lines else "<empty log>"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ The one protocol for every exchange between agents; `coordinate`, `triage`,
|
|||||||
permission: never ask a peer to run what your own session is denied; report
|
permission: never ask a peer to run what your own session is denied; report
|
||||||
it to the operator.
|
it to the operator.
|
||||||
- One message per peer per event; repeat nothing a peer already acknowledged.
|
- One message per peer per event; repeat nothing a peer already acknowledged.
|
||||||
|
- Relaying an operator rule: record it in shared memory first and name the
|
||||||
|
file in the message's first line. A recipient records it only when no file
|
||||||
|
is named, and only after `council.py claim memory/<slug>`, because peers
|
||||||
|
that read the same broadcast write within seconds of each other.
|
||||||
- Release every claim when its work ends; a forgotten claim blocks the work
|
- Release every claim when its work ends; a forgotten claim blocks the work
|
||||||
for everyone.
|
for everyone.
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> N
|
|||||||
"session": sid,
|
"session": sid,
|
||||||
"runtime": runtime,
|
"runtime": runtime,
|
||||||
"name": live_sessions().get(sid),
|
"name": live_sessions().get(sid),
|
||||||
"since": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
"since": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
|
||||||
"beat": time.time(),
|
"beat": time.time(),
|
||||||
"work": work,
|
"work": work,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ class TestCiFailures(unittest.TestCase):
|
|||||||
'TASK [web-app-x : Wait for health] **** fatal: [localhost]: FAILED! => {"msg": "timeout <n>"}',
|
'TASK [web-app-x : Wait for health] **** fatal: [localhost]: FAILED! => {"msg": "timeout <n>"}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_lint_finding_beats_make_error_and_later_steps(self):
|
||||||
|
text = (
|
||||||
|
"Ran 41 tests in 1.0s\n"
|
||||||
|
"OK\n"
|
||||||
|
"UP017 [*] Use `datetime.UTC` alias\n"
|
||||||
|
" --> skills/democracy/scripts/council.py:92:40\n"
|
||||||
|
"Found 1 error.\n"
|
||||||
|
"make: *** [Makefile:15: test] Error 1\n"
|
||||||
|
"##[error]Process completed with exit code 2.\n"
|
||||||
|
"Post job cleanup.\n"
|
||||||
|
"SHA256 digest of uploaded artifact is 0123456789abcdef\n"
|
||||||
|
)
|
||||||
|
self.assertEqual(self.ci.signature(text), "UP<n> [*] Use `datetime.UTC` alias")
|
||||||
|
|
||||||
def test_generic_exit_code_line_is_not_the_signature(self):
|
def test_generic_exit_code_line_is_not_the_signature(self):
|
||||||
text = "Error: disk full\n##[error]Process completed with exit code 1.\n"
|
text = "Error: disk full\n##[error]Process completed with exit code 1.\n"
|
||||||
self.assertEqual(self.ci.signature(text), "Error: disk full")
|
self.assertEqual(self.ci.signature(text), "Error: disk full")
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""update.sh must reach every locked source and install for Claude Code only."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
UPDATE = REPO_ROOT / "scripts" / "update.sh"
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which("jq") and shutil.which("bash"), "jq or bash missing")
|
||||||
|
class TestUpdate(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.bin = Path(self.tmp.name)
|
||||||
|
self.log = self.bin / "calls.log"
|
||||||
|
npx = self.bin / "npx"
|
||||||
|
npx.write_text(f'#!/bin/sh\ncat >/dev/null\necho "$@" >> "{self.log}"\n', encoding="utf-8")
|
||||||
|
npx.chmod(0o755)
|
||||||
|
for tool in ("jq", "sort"):
|
||||||
|
(self.bin / tool).symlink_to(shutil.which(tool))
|
||||||
|
|
||||||
|
def test_every_source_is_updated_for_claude_code_only(self):
|
||||||
|
lock = json.loads((REPO_ROOT / "skills-lock.json").read_text(encoding="utf-8"))
|
||||||
|
sources = sorted({entry["source"] for entry in lock["skills"].values()})
|
||||||
|
subprocess.run(
|
||||||
|
["bash", str(UPDATE)],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
env={"PATH": f"{self.bin}:/usr/bin:/bin"},
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
calls = self.log.read_text(encoding="utf-8").splitlines()
|
||||||
|
self.assertEqual([c.split()[-1] for c in calls], sources)
|
||||||
|
for call in calls:
|
||||||
|
self.assertIn("--agent claude-code", call)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user