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
77 changes: 77 additions & 0 deletions .github/scripts/changelog-section.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use strict";

/**
* Extract a version's section from CHANGELOG.md for the release body.
*
* The release workflow passes the section to `gh release create
* --generate-notes --notes-file <section>`; GitHub prepends the file to the
* auto-generated PR list. Kept as a pure module so the heading grammar can be
* unit-tested without Actions.
*
* Heading grammar (Keep a Changelog):
* ## [0.2.27] - 2026-09-13 (ASCII hyphen, current)
* ## [0.2.16] — 2026-08-30 (em dash, 0.2.1-0.2.16)
* The section ends at the next `#`/`##` heading or a `[label]:` link
* definition (the compare-link footer).
*/

/** Escape a version string for use inside a RegExp. */
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
* Return the raw `## [version]` section (heading included, trailing
* whitespace trimmed). Throws Error with `.code` "MISSING" when no heading
* matches and "EMPTY" when the section has no body.
*/
function extractChangelogSection(changelogText, version) {
if (!version) {
const err = new Error("version is empty");
err.code = "MISSING";
throw err;
}
const startRe = new RegExp(`^## \\[${escapeRegExp(version)}\\](?:\\s|$)`);
const lines = String(changelogText).split(/\r?\n/);
const start = lines.findIndex((line) => startRe.test(line));
if (start < 0) {
const err = new Error(`CHANGELOG.md has no "## [${version}]" section`);
err.code = "MISSING";
throw err;
}
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (/^#{1,2}\s/.test(lines[i]) || /^\[[^\]]+\]:\s/.test(lines[i])) {
end = i;
break;
}
}
const section =
lines
.slice(start, end)
.join("\n")
.replace(/[ \t]+$/gm, "")
.replace(/\n+$/, "") + "\n";
const body = section.replace(/^##[^\n]*\n/, "").trim();
if (!body) {
const err = new Error(`CHANGELOG.md section for ${version} is empty`);
err.code = "EMPTY";
throw err;
}
return section;
}

if (require.main === module) {
const version = process.argv[2];
const path = process.argv[3] || "CHANGELOG.md";
try {
const section = extractChangelogSection(require("node:fs").readFileSync(path, "utf8"), version);
process.stdout.write(section);
process.stdout.write("\n---\n");
} catch (err) {
console.error(err.message);
process.exit(1);
}
}

module.exports = { extractChangelogSection, escapeRegExp };
87 changes: 87 additions & 0 deletions .github/scripts/changelog-section.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use strict";

const fs = require("node:fs");
const path = require("node:path");
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { extractChangelogSection } = require("./changelog-section.cjs");

const SAMPLE = [
"# Changelog",
"",
"## [Unreleased]",
"",
"## [0.2.27] - 2026-09-13",
"",
"### Added",
"",
"- Logic analysis ships",
"",
"## [0.2.16] — 2026-08-30",
"",
"### Fixed",
"",
"- Em-dash heading entry",
"",
"## [0.2.20] - 2026-08-01",
"",
"- Twenty",
"",
"## [0.2.2] - 2026-07-01",
"",
"- Two",
"",
"[Unreleased]: https://example/compare/v0.2.27...HEAD",
"# Changelog",
"",
"## 0.2.7 (2026-08-22)",
"",
"- leftover document",
"",
].join("\n");

describe("extractChangelogSection", () => {
it("extracts a hyphen-dated section with its body", () => {
const out = extractChangelogSection(SAMPLE, "0.2.27");
assert.ok(out.startsWith("## [0.2.27] - 2026-09-13"));
assert.ok(out.includes("- Logic analysis ships"));
assert.ok(!out.includes("0.2.16"), "stops before the next section");
assert.ok(out.endsWith("\n"));
});

it("accepts em-dash headings", () => {
const out = extractChangelogSection(SAMPLE, "0.2.16");
assert.ok(out.includes("- Em-dash heading entry"));
});

it("matches the exact version, never a prefix", () => {
const out = extractChangelogSection(SAMPLE, "0.2.2");
assert.ok(out.includes("- Two"));
assert.ok(!out.includes("Twenty"), "0.2.2 must not steal the 0.2.20 section");
});

it("stops at compare-link footers and repeated top headings", () => {
const out = extractChangelogSection(SAMPLE, "0.2.2");
assert.ok(!out.includes("[Unreleased]:"), "link footer excluded");
assert.ok(!out.includes("leftover document"), "second document excluded");
});

it("fails closed when the section is missing", () => {
assert.throws(() => extractChangelogSection(SAMPLE, "0.2.25"), /no "## \[0\.2\.25\]" section/);
});

it("fails closed when the section has no body", () => {
assert.throws(() => extractChangelogSection(SAMPLE, "Unreleased"), /is empty/);
});

it("fails closed on an empty version", () => {
assert.throws(() => extractChangelogSection(SAMPLE, ""), /version is empty/);
});

it("extracts the real 0.2.27 section from this repo's CHANGELOG.md", () => {
const real = fs.readFileSync(path.join(__dirname, "..", "..", "CHANGELOG.md"), "utf8");
const out = extractChangelogSection(real, "0.2.27");
assert.ok(out.includes("logic-analysis"), "curated notes reach the release body");
assert.ok(!out.includes("## [0.2.26]"), "does not bleed into the previous release");
});
});
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,14 @@ jobs:
set -euo pipefail
tag="v${VERSION}"
if ! gh release view "$tag" >/dev/null 2>&1; then
args=(--title "codexclaw $tag" --target "$GITHUB_SHA" --generate-notes)
# Curated notes first: extract this version's CHANGELOG.md section
# and pass it via --notes-file. GitHub prepends it to the
# --generate-notes PR list (the flags compose; the exclusive pair
# is --notes-from-tag). Fails closed when the section is missing
# or heading-only, so a forgotten CHANGELOG entry blocks publish.
notes="$(mktemp)"
node .github/scripts/changelog-section.cjs "$VERSION" > "$notes"
args=(--title "codexclaw $tag" --target "$GITHUB_SHA" --generate-notes --notes-file "$notes")
kind="$(node bin/codexclaw.mjs release classify --version "$VERSION")"
[ "$kind" = prerelease ] && args+=(--prerelease)
gh release create "$tag" "${args[@]}"
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C142_passing-brightgreen" alt="3,142 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C150_passing-brightgreen" alt="3,150 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C142_passing-brightgreen" alt="3,142 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C150_passing-brightgreen" alt="3,150 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C142_passing-brightgreen" alt="3,142 tests passing">
<img src="https://img.shields.io/badge/tests-3%2C150_passing-brightgreen" alt="3,150 tests passing">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-28-blue" alt="28 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
57 changes: 57 additions & 0 deletions devlog/_fin/260913_logic_analysis_skill/095_done.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 095 — done: logic-analysis skill unit

## Outcome: DONE

All four goal criteria met with fresh evidence; goalplan E8 gate passes
(`cxc loop validate` OK at close).

## What shipped (v0.2.27)

- **New reference** `plugins/codexclaw/skills/dev-debugging/references/logic-analysis.md`
(142 lines): the logic-analysis loop (precise question → isolate → domain
language → inventory the callable surface → slice → hypothesize from
names/strings/errors → observe static AND dynamic → prove by writing a
client), controlled mutation (one variable, canary, two encodings), the
incremental UNKNOWN-field model, a technique routing table with the
agent-accessible CLI slice, the honest human-lab boundary, the 9-rung
anti-give-up escalation ladder, and authorization/hostile-code limits.
Core rule: "I can't analyze this" is a skipped loop, not a limit.
- **dev-debugging SKILL.md** (414 → 431 lines): frontmatter triggers
(로직 파악, 뜯어봐, reverse engineer, ...), boundary route, compact Logic
Analysis section, references-table row, compact-summary item (8).
- **Cross-links** (user-requested mid-release): `cxc-dev` §2 + routing table
row; `cxc-search` Korean Intent Guard rule 1 fourth target class.
- **CHANGELOG** 0.2.27 entry; version bump across 16 surfaces.

## Evidence

- Analysis synthesis with `/tmp` clone `path:line` citations: `001_analysis_synthesis.md`
(three grok-4.6 explorer subagents: mytechnotalent, wtsxDev, Z0F).
- A-phase: reviewer da454926, round 1 GO-WITH-FIXES (1 blocker: Edit B
literal/anchor) → folded → round 2 PASS.
- Validation: gate OK; inventory+gate suites 21 pass / 0 fail; receipts at
`.codexclaw/evidence/0d7e3cc3-8bdf-4645-8cf6-a7d42a2fb2ab/test-receipt.json`.
- Delivery: PR #167 merged `b9e68930`; PR #168 merged `7e998908`; Release
run 34748229204 success → GitHub Release v0.2.27 (3 assets, non-prerelease,
target 7e998908, published 2026-09-13T08:41:40Z). A duplicate Release
dispatch queued in the same minute was cancelled; the fail-closed
re-publish guard never fired.

## What did not improve / died hypotheses (LOOP-PESSIMIST-01)

- Initial assumption "deploy = merge to main" died: the Release workflow is
dispatch-only with `expected_sha` + version-surface gates, so a 0.2.27
prep commit + second PR was required. Recorded in 020 for the next release.
- The wtsxDev awesome-list turned out to be a binary-malware map with no
source-available or web/API lane — its value to the unit is the routing
table and the honest-lab boundary, not technique content.
- Not done (out of scope): cursorclaw/zclaw plugin port sync of the edited
skills; the installed cursor plugin copy still carries the pre-0.2.27
dev-debugging until the port pipeline runs.

## Commits

- `352acc24` docs(plan): roadmap + synthesis
- `1c3ce3dc` feat(dev-debugging): logic-analysis reference
- `dfafaecf` chore(release): prepare 0.2.27
- `b8d16f85` feat(skills): dev + search cross-links
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"scripts": {
"build": "node plugins/codexclaw/scripts/build.mjs",
"gate": "node plugins/codexclaw/scripts/gate.mjs",
"test": "node plugins/codexclaw/scripts/test.mjs \"plugins/codexclaw/components/pabcd-state/test/*.test.ts\" \"plugins/codexclaw/components/config-guard/test/*.test.ts\" \"plugins/codexclaw/components/cxc-ops/test/*.test.ts\" \"plugins/codexclaw/components/recall/test/*.test.ts\" \"plugins/codexclaw/components/provider-bridge/test/*.test.ts\" \"plugins/codexclaw/components/subagent-config/test/*.test.ts\" \"plugins/codexclaw/components/messenger-bridge/test/*.test.ts\" \"plugins/codexclaw/components/skill-search/test/*.test.ts\" \"plugins/codexclaw/components/bg-wake/test/*.test.ts\" \"plugins/codexclaw/gui/test/*.test.ts\" \"plugins/codexclaw/test/*.test.mjs\" \".github/scripts/pr-labeler.test.cjs\" \".github/scripts/closed-pr-branch-cleanup.test.cjs\"",
"test": "node plugins/codexclaw/scripts/test.mjs \"plugins/codexclaw/components/pabcd-state/test/*.test.ts\" \"plugins/codexclaw/components/config-guard/test/*.test.ts\" \"plugins/codexclaw/components/cxc-ops/test/*.test.ts\" \"plugins/codexclaw/components/recall/test/*.test.ts\" \"plugins/codexclaw/components/provider-bridge/test/*.test.ts\" \"plugins/codexclaw/components/subagent-config/test/*.test.ts\" \"plugins/codexclaw/components/messenger-bridge/test/*.test.ts\" \"plugins/codexclaw/components/skill-search/test/*.test.ts\" \"plugins/codexclaw/components/bg-wake/test/*.test.ts\" \"plugins/codexclaw/gui/test/*.test.ts\" \"plugins/codexclaw/test/*.test.mjs\" \".github/scripts/pr-labeler.test.cjs\" \".github/scripts/closed-pr-branch-cleanup.test.cjs\" \".github/scripts/changelog-section.test.cjs\"",
"smoke": "node plugins/codexclaw/scripts/platform-smoke.mjs"
},
"overrides": {
Expand Down
Loading