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
2 changes: 1 addition & 1 deletion .claude/sdlc/prompts/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Spec: read `{{ARTIFACT}}` in full, then the intent it names. Read `AGENTS.md` (i

Explore the source with Read, Grep and Glob until you can name every file that changes.

Write `docs/plans/{{SLUG}}.md` using `docs/plans/TEMPLATE.md` exactly: keep its frontmatter keys, set `status: draft`, `spec: {{ARTIFACT}}`, `generated_by: sdlc-loop`, `build: pending`. Sections:
Write `docs/plans/{{SLUG}}.md` using `docs/plans/TEMPLATE.md` exactly: keep its frontmatter keys, set `status: draft`, `spec: {{ARTIFACT}}`, `build: pending`. Do not add author or generator attribution. Sections:

- Files that change: every path, marked new / modified / deleted, one line each on what changes there. Tests count as files.
- Order of work: numbered steps, smallest vertical slice first (one path that works end to end before widening).
Expand Down
2 changes: 1 addition & 1 deletion .claude/sdlc/prompts/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ Intent: read `{{ARTIFACT}}` in full.
Constraints you must apply, in this order of authority (read each one; quote it when you flag a concern):
{{CONSTRAINTS}}

Write `docs/specs/{{SLUG}}.md` using `docs/specs/TEMPLATE.md` exactly: keep its frontmatter keys, set `status: draft`, `intent: {{ARTIFACT}}`, `generated_by: sdlc-loop`. Sections: Problem (restated from the intent in the product's vocabulary), Requirements (numbered, each testable), Design (which existing modules, routes, components and stores change; no parallel implementation, no new runtime), Data and rights, Security and privacy, Out of scope, Concerns (every place the intent conflicts with a constraint above, or two constraints conflict with each other; the product owner resolves these before engineering sees the spec), Open questions (carried forward from the intent plus new ones), Acceptance (what a reviewer checks to accept this spec).
Write `docs/specs/{{SLUG}}.md` using `docs/specs/TEMPLATE.md` exactly: keep its frontmatter keys, set `status: draft`, `intent: {{ARTIFACT}}`. Do not add author or generator attribution. Sections: Problem (restated from the intent in the product's vocabulary), Requirements (numbered, each testable), Design (which existing modules, routes, components and stores change; no parallel implementation, no new runtime), Data and rights, Security and privacy, Out of scope, Concerns (every place the intent conflicts with a constraint above, or two constraints conflict with each other; the product owner resolves these before engineering sees the spec), Open questions (carried forward from the intent plus new ones), Acceptance (what a reviewer checks to accept this spec).

Rules: do not change any file other than `docs/specs/{{SLUG}}.md`. Do not write code. Do not invent capabilities the constraints exclude. Keep the spec under 250 lines.
122 changes: 55 additions & 67 deletions .github/workflows/sdlc-loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,52 @@ permissions:
contents: read

jobs:
pending:
name: Which artifacts are accepted and waiting
sdlc:
name: Run pending stages in order
runs-on: ubuntu-latest
outputs:
items: ${{ steps.scan.outputs.items }}
ready: ${{ steps.secrets.outputs.ready }}
env:
GH_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN }}
timeout-minutes: 90
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
token: ${{ secrets.SDLC_GITHUB_TOKEN || github.token }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: .nvmrc
cache: npm
- id: scan
env:
GH_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN || github.token }}
SDLC_GITHUB_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN }}
run: |
pending_file="$RUNNER_TEMP/sdlc-pending.json"
if ! node scripts/sdlc/next-stage.mjs > "$pending_file"; then
if [ -z "$SDLC_GITHUB_TOKEN" ]; then
echo "::error::SDLC_GITHUB_TOKEN is not set; the accepted-stage scan could not read GitHub state."
fi
exit 1
fi
node --input-type=module - "$pending_file" <<'NODE'
import { appendFileSync, readFileSync } from "node:fs";
const items = JSON.parse(readFileSync(process.argv[2], "utf8"));
if (!Array.isArray(items)) throw new Error("next-stage did not produce a JSON array");
const hasPending = items.length > 0;
const hasBuild = items.some((item) => item?.stage === "build");
appendFileSync(process.env.GITHUB_OUTPUT, `has_pending=${hasPending}\nhas_build=${hasBuild}\n`);
const summary = [
"## Pending stages",
"",
...(hasPending
? items.map((item) => `- \`${item.stage}\` \`${item.slug}\` — \`${item.artifact}\``)
: ["Nothing pending: every accepted artifact already has its next stage. No provider install or model call was run."]),
"",
].join("\n");
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
NODE
- id: secrets
if: steps.scan.outputs.has_pending == 'true'
env:
GH_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Expand All @@ -51,73 +80,32 @@ jobs:
node scripts/sdlc/agent.mjs --check || ready=false
if [ -z "$GH_TOKEN" ]; then ready=false; echo "::error::SDLC_GITHUB_TOKEN secret is not set (PRs opened with github.token get no CI)."; fi
echo "ready=$ready" >> "$GITHUB_OUTPUT"
[ "$ready" = true ] || echo "**Blocked: a required secret is missing.** Run scripts/sdlc/bootstrap.sh. See docs/sdlc/LOOP.md." >> "$GITHUB_STEP_SUMMARY"
- id: scan
run: |
items="$(node scripts/sdlc/next-stage.mjs)"
echo "items=$items" >> "$GITHUB_OUTPUT"
{
echo "## Pending stages"
echo
if [ "$items" = "[]" ]; then echo "Nothing pending: every accepted artifact already has its next stage."; else echo '```'; node scripts/sdlc/next-stage.mjs --human; echo '```'; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Refuse to run stages without the secrets
if: steps.secrets.outputs.ready != 'true' && steps.scan.outputs.items != '[]'
run: exit 1

stage:
name: ${{ matrix.item.stage }} ${{ matrix.item.slug }}
needs: pending
if: needs.pending.outputs.items != '[]' && needs.pending.outputs.ready == 'true'
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
max-parallel: 1
fail-fast: false
matrix:
item: ${{ fromJSON(needs.pending.outputs.items) }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GH_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
token: ${{ secrets.SDLC_GITHUB_TOKEN }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: .nvmrc
cache: npm
if [ "$ready" != true ]; then
echo "**Blocked: a required secret is missing.** Run scripts/sdlc/bootstrap.sh. See docs/sdlc/LOOP.md." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: Install the configured provider CLI (sdlc/config.json agent.provider)
if: steps.scan.outputs.has_pending == 'true' && steps.secrets.outputs.ready == 'true'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
run: node scripts/sdlc/agent.mjs --install
# PROJECT TOOLCHAIN for the build stage: everything `npm run verify` needs
# (dependencies, browsers, language runtimes). Edit for the repository.
- name: Toolchain for the build stage
if: matrix.item.stage == 'build'
if: steps.scan.outputs.has_build == 'true' && steps.secrets.outputs.ready == 'true'
run: |
npm ci
npx playwright install --with-deps chromium
- name: Run the stage
if: steps.scan.outputs.has_pending == 'true' && steps.secrets.outputs.ready == 'true'
env:
STAGE: ${{ matrix.item.stage }}
SLUG: ${{ matrix.item.slug }}
ARTIFACT: ${{ matrix.item.artifact }}
run: node scripts/sdlc/run-stage.mjs --stage "$STAGE" --slug "$SLUG" --artifact "$ARTIFACT"
# The run record is the model's full transcript (it quotes repository
# content it read). Anyone who can read this repository's Actions can
# download it (on a public repository: everyone); it is what you read
# when a stage fails. Kept 14 days.
- name: Keep the run record
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: sdlc-run-${{ matrix.item.stage }}-${{ matrix.item.slug }}
path: |
.sdlc-run/
.verify/
retention-days: 14
if-no-files-found: ignore
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GH_TOKEN: ${{ secrets.SDLC_GITHUB_TOKEN }}
run: node scripts/sdlc/run-pending.mjs "$RUNNER_TEMP/sdlc-pending.json"
# Raw model records stay on the disposable runner. Public diagnostics
# are the step log, pending-stage summary and resulting pull request.
16 changes: 2 additions & 14 deletions .github/workflows/sdlc-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,7 @@ jobs:
PR: ${{ github.event.pull_request.number }}
BASE: ${{ github.event.pull_request.base.ref }}
run: node scripts/sdlc/review.mjs --request "$PR" --base "$BASE"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: review-${{ github.event.pull_request.number }}-${{ github.run_id }}
path: .sdlc-run/
retention-days: 14
if-no-files-found: ignore
# Publish findings through the PR note, not the raw model transcript.

review-matrix:
name: ${{ matrix.cell.pass }} pass, ${{ matrix.cell.dir }}
Expand Down Expand Up @@ -174,10 +168,4 @@ jobs:
PASS: ${{ matrix.cell.pass }}
DIR: ${{ matrix.cell.dir }}
run: node scripts/sdlc/review.mjs --request "$PR" --base "$BASE" --pass "$PASS" --dir "$DIR"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: review-${{ github.event.pull_request.number }}-${{ matrix.cell.pass }}-${{ strategy.job-index }}-${{ github.run_id }}
path: .sdlc-run/
retention-days: 14
if-no-files-found: ignore
# Publish findings through the PR note, not the raw model transcript.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ All notable changes to MeMesh are documented here.

### Fixed

- **The implementation-plan evaluation now starts after intent and spec
acceptance.** The original question asked for the first feature artifact,
so an answer naming intent could fail a checker expecting plan and Proof.
The checker is unchanged. See
`docs/postmortems/2026-09-14-sdlc-plan-eval.md`.
- **SDLC pending stages stay in one Actions job.** The first accepted intent
could not reach its spec stage when Actions suppressed the JSON job output.
A runner-local file now feeds stages sequentially; malformed input and failed
stages stop explicitly. Artifact publication preserves conflicting branches.
Loop/review jobs no longer upload raw model records; spec/plan templates
omit generator credits.
See `docs/postmortems/2026-09-14-sdlc-pending-output.md` for the missing hosted
seam coverage and the required spec-PR readback.
- **The isolated release suite now owns its npm cache.** The release runner
no longer inherits a maintainer's `~/.npm` cache when nested tests execute
`npm pack`, so local ownership damage cannot turn an otherwise isolated
Expand Down
1 change: 0 additions & 1 deletion docs/plans/TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
title: <title from the spec>
status: draft
spec: docs/specs/<slug>.md
generated_by: sdlc-loop
build: pending
---

Expand Down
59 changes: 59 additions & 0 deletions docs/plans/sdlc-first-cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Make the first accepted intent reach a reviewable specification
status: draft
build: manual
---

# First useful SDLC cycle

## Outcome

The accepted intent for observation-forget-survives-stop reaches a draft spec
pull request. The specification remains subject to independent review and a
person's acceptance. This change does not implement the forget behavior.

## Changes

- Keep the pending-stage list within one Actions job and execute it in order,
reusing the scanner and stage runner. Do not transmit the JSON list as a
cross-job output or weaken secret masking.
- Record an empty list as no work. Refuse malformed input, missing required
credentials, and failed stages explicitly. Stop after a failed stage.
- Publish artifact branches with ordinary Git push; preserve a conflicting
remote branch. Do not add model credits to commits, PR bodies or review
headings. An empty review response is a failure.
- Keep raw loop/review model records on the runner; publish diagnostics and
findings instead. Remove generator attribution from spec/plan templates
and the prompts that fill them.
- Do not pass provider or repository credentials to package installation.
Persist Codex login during setup; filter known model credential variables
to the selected provider, with repository token variables reserved for build.
- Select named models in the existing configuration: smaller models for
bounded drafting and implementation, a different model for planning and
independent review. This is routing, not a token-budget guarantee.
- Scope the no-plan-no-build evaluation to implementation after accepted
intent and spec. Keep its plan-path and Proof checker unchanged.

## Proof

- `npm run sdlc:test` exercises ordered real subprocess execution, no work,
invalid input and first-failure termination; shell-like data cannot become
shell commands.
- A local bare Git remote accepts initial artifact publication and rejects a
conflicting second attempt with its original commit unchanged.
- Review formatting rejects empty output and adds no model credits; artifact
status and changed-file checks remain covered.
- `npm run verify` exits 0 for the exact candidate tree.
- The model-backed no-plan-no-build case names the implementation plan and
Proof. Its original ambiguous scenario can correctly answer intent first;
the revised scenario must distinguish that from permission to implement.
- After authorized merge, one hosted run creates the expected spec PR; read
back its branch, changed file, draft frontmatter and contents. Without that
run, hosted behavior remains unverified.

## Risks and limits

Sequential execution stops at the first failed stage, leaving later items for
inspection and a subsequent run. Existing remote branches are never replaced
by force. Credentials, branch-protection policy, product fixes, broader golden
journey coverage and release promotion are outside this change.
33 changes: 33 additions & 0 deletions docs/postmortems/2026-09-14-sdlc-pending-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SDLC pending output prevented the first spec stage

## Symptom

After the accepted intent merged, Actions run 34782680665 failed. Its pending
job succeeded, but no spec job or PR was created.

## Root cause

The workflow passed a JSON pending-stage list through a job output into a
downstream matrix. The runner suppressed that output with:

```text
Skip output 'items' since it may contain secret.
```

The downstream stage therefore had no usable list. The exact secret-mask
match was not inspected; credential contents are not needed to correct the
workflow's dependence on this transport.

## Why existing gates missed it

Scanner and stage tests exercised each script, but did not exercise the
GitHub runner's handling of job outputs. The earlier hosted run had no pending
stage and did not prove that a nonempty list could reach a stage.

## Gate added

Pending items remain in a runner-local file and execute sequentially within
the same job. Process-level tests cover empty and malformed lists, ordering,
argument handling and stopping after failure. These tests do not emulate
GitHub secret masking; hosted acceptance requires a new successful run and
readback of the resulting spec PR.
32 changes: 32 additions & 0 deletions docs/postmortems/2026-09-14-sdlc-plan-eval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Implementation-plan evaluation asked about the wrong stage

## Symptom

Actions run 34807191993 authenticated and ran both model cases, but
no-plan-no-build reported that the answer did not name the plan path and
Proof. The other case passed.

## Root cause

The prompt asked what must be produced first for a new feature. The checker
expected the implementation plan, while the documented SDLC starts with
intent and then spec. A local replay on commit 8eea7ee reproduced this
mismatch: the model read contributor and loop instructions, answered intent
first, and exited 0; the checker exited 1. The parsed answer matched the
CLI's final-message file. The original hosted answer was not retained, so
this replay establishes a reproducible cause, not its exact hosted wording.

## Why existing gates missed it

Parser tests validate event extraction. A previous successful model answer
did not establish that the question had only one policy-consistent answer.
The prompt omitted the stage precondition required by its checker.

## Gate correction

The scenario now begins after intent and specification acceptance and asks
for the artifact needed before implementation. The existing requirement to
read project guidance and name the plan path and Proof remains unchanged;
the prompt does not supply those answer terms. Model-backed evaluation must
exercise this scenario. This evaluation concerns contributor behavior, not
product runtime or completion of the hosted SDLC loop.
Loading
Loading