Symphony (oh-my-symphony) gained its dark-factory cycle on branch
feat/dark-factory-cycle (from dev): a software request typed into the
web chat becomes a strict intent card, the operator approves once, and the
server files the request ticket into the pipeline. That approval is the only
human gate; the deep preset does the rest. Design and plan live in
docs/plans/2026-09-05-dark-factory-cycle-design.md and
docs/plans/2026-09-05-dark-factory-cycle-implementation-plan.md.
Every gate is green on the branch:
- ruff ✓ · pyright 0 errors ✓ · i18n 566 keys (en/ko) ✓
- Full pytest suite 2476 passed, 14 skipped (baseline before the branch: 2415 passed, 14 skipped)
- Live run-path check:
symphony service starton a scratch project, a realclaudechat session in Q&A mode produced the intent card from one request, the bareapprovereply filedREQ-1(request: python-todo-cli) inTodoand wrote.sdlc/work/python-todo-cli/intent.mdwith its## Approvalsection.
| Area | Content |
|---|---|
| Intent gate | src/symphony/intent.py (marker parser, IntentAction, file_intent_request), chat.py (confirm_intent, intent_for_reply, supersede/expiry/prune, agent notice), webapi.py (POST /api/v1/chat/sessions/{sid}/intent/{action_id}/approve, approve reply routing), web card (app.js, i18n.js, style.css) |
| Pipeline | _BOARD_PREAMBLE rewritten around _INTENT_PROTOCOL; deep intake.md consumes the intent (Track: micro → Plan); project-setup proposals accept preset: deep and projects.create_or_adopt_project(preset=) applies it before the initial commit |
| Bugs | #29 per-dispatch env in BackendInit.env; #28 RetryEntry.touched_files snapshot for the conflict pre-check; #30 utils/atomic_json.py rename retry; #32 per-workflow state file names |
| Debt | orchestrator/worker_exit.py (worker-exit state machine, 1009 lines) and orchestrator/attempt.py (_run_agent_attempt phases, 1095 lines) extracted from core.py (11349 → 9815 lines); claude and pi backends now subclass per_turn.JsonlStreamBackend (claude_code.py 503 → 267, pi.py 666 → 386) |
| Docs | README (en/ko) "Chat intake" section, CONTEXT.md (Intent proposal, Intent approval, Track), docs/PIPELINE.md intent gate, skills/symphony-skill/reference/operations.md, docs/architecture.md module table, CHANGELOG |
Scratch registry: SYMPHONY_PROJECTS_FILE=/tmp/symphony-intent-smoke.ntna/projects.json;
the deep project lives at /tmp/symphony-intent-smoke.ntna/todo-app
(service port 9999, log log/symphony.log, stop with
symphony service stop ./WORKFLOW.md from that directory). The pipeline was
still finishing when this handoff was written; check the board there.
| Step | What happened | Evidence |
|---|---|---|
| Deep project from chat | On the default smoke board, an edit-mode codex chat proposed a separate project with preset: deep; one confirmation created and registered todo-cli with the eight lanes |
e2e-step2-codex.log, projects.json |
| Intent gate | On the new board, a Q&A codex chat produced the intent card from one request; a bare approve reply filed REQ-1 in Intake with request: python-cli-todo and wrote .sdlc/work/python-cli-todo/intent.md |
e2e-step3.log, kanban/REQ-1.md |
| Request lanes | REQ-1 walked Intake → Research → Plan → Review → Done with no operator question; Review passed; the branch merged (auto_merge_completed) |
log/symphony.log |
| Decomposition | Plan spawned BUILD-1..3, QA-1, VERIFY-1, DOCUMENT-1 and wrote docs/req/python-cli-todo/{brief,research,plan,contracts,review}.md plus release-contract.yaml |
scratch main |
| Builds | BUILD-1, BUILD-2, BUILD-3 each reached Done and merged in order; QA-1 reached Done at 23:10 UTC; VERIFY-1 and DOCUMENT-1 were still pending |
monitor log |
| Delivered app | git archive main of the scratch project: todo.py ($TODO_FILE override, JSON next to the script), tests/test_{storage,cli,readme}.py, README; pytest -q → 26 passed; todo.py add "buy milk" then list prints [ ] 1. buy milk |
export run at 23:09 UTC |
Findings from the run (not code defects in Symphony unless noted):
- The claude CLI hit the account's session limit; the chat turn failed
cleanly (
turn_failedin the transcript) and no card was produced. - codex hit its usage limit mid-Plan; the worker was auto-paused
(
worker_error_auto_paused). Recovery that worked: editagent.kindinWORKFLOW.mdand the ticket's ownagent.kind(the file tracker stamps the dispatched kind on the ticket and it overrides the board default), thenPOST /api/v1/REQ-1/resume.WORKFLOW.mdedits are picked up live; no restart was needed. ~/.opencode/bin/opencodeon this host is a symlink to a broken npm postinstall stub; the real 1.18.18 binary is~/.opencode/bin/opencode1. The scratch workflow uses that absolute path inopencode.command. Product follow-up worth considering:symphony doctorcould run<agent> --versionfor the configured kind and flag a non-zero exit.- Wall clock: intent approval 22:29 UTC → REQ-1 Done 22:59 → three builds merged by 23:07 → QA Done 23:10, with ~20 minutes lost to the two quota failures.
-
Deep boards from the example workflow cannot dispatch the app-release verifier (found in the E2E, 23:11 UTC).
WORKFLOW.file.example.mdshipsfeature_base_branch: ""andauto_merge_target_branch: ""(meaning "the current branch");symphony doctoraccepts that for the deep preset ('' == ''), butorchestrator/release_contracts.pyrefuses to bind the verifier:release_dispatch_refused ... configured target_branch must name a resolvable local branch: '', every tick, whileVERIFY-1sits inVerifyand the finalizer waits on it. Setting both keys to"main"in the scratch workflow (live reload) dispatchedVERIFY-1within a tick. Fix candidates: resolve an empty configured target to the repository's current branch inside the binder (matching auto-merge semantics), or have thepreset: deepbootstrap write the repository's initial branch into both keys; either waycheck_deep_preset_merge_contractand the binder must agree. Add a regression test that binds a verifier on a board whose configured target is empty. -
#31 codex sandbox vs symlinked board files. Current code already injects resolved symlink targets and git admin dirs (
backends/codex.py:_scan_workspace_symlinks). The 2026-05 failure has no deterministic repro; it needs a live codex run against a symlinked file board before it can be closed or fixed. -
Next
core.pyextractions (in order):_enforce_app_release_transition_inner(~420 lines) intorelease_cycle.py;_on_tick(~400);_reconcile_one(~345). Follow theworker_exit.pyconvention: module-level functions that takeorchexplicitly,_core()late binding for names tests patch oncore, full suite as the gate. -
win32
process_identity()still returnsNone(kills stay ungated warn-once on Windows). AGetProcessTimes-based fingerprint in_shell.pywould enable pid-reuse protection. -
Deferred UX items from the 2026-08-23 audit: scrollable HelpScreen, modal focus trap, web-board rendering for Linear/Jira trackers, TUI↔web action parity.
-
Intent gate follow-ups: Linear/Jira boards are not covered (proposals parse only on file boards); the kit's
tools/tripwire.shhedging scan is not run on the intent body; this repository's own.sdlc/config.mdstill has nolazymode:line (default 1) — setlazymode: 3only on the owner's explicit instruction. -
tests/test_orchestrator_dispatch.py::test_startup_reclaim_terminates_live_recorded_orphan_agent_groupis timing-flaky under CPU load (it kills a real process group). It passed in every full-suite run on this branch; rerun it alone before treating a failure as a regression. -
Parallel test suite (pytest-xdist) is not isolation-clean yet.
pytest -n autofinishes in ~72 s instead of ~234 s, but two tests fail under parallel load and pass serially:tests/test_workspace.py::test_setup_worktree_script_supports_linked_workflow_dir[mkdir]andtests/test_orchestrator_release_contract_integration.py::test_reconcile_terminal_release_holds_lease_until_cleanup_finishes(anasynciotimeout). Make those two load-tolerant before addingpytest-xdistto the dev extra and-n autoto CI; nothing in the repo depends on xdist today.
- Run tests with
.venv/bin/python -m pytest -q(barepythonis not the project interpreter). symphonyis not on the login-shell PATH on this host; workers receiveSYMPHONY_CLI=<venv>/bin/symphony, and the shipped prompts use${SYMPHONY_CLI:-symphony}.- Scratch project used for the live check: created with
SYMPHONY_PROJECTS_FILE=<tmp>/projects.json symphony project create ... --port 9997so the global project registry stayed untouched.