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 openspec/changes/prefer-triad-in-project-new/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-07
135 changes: 135 additions & 0 deletions openspec/changes/prefer-triad-in-project-new/clarifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Clarifications

_Captured during proposal council review. These constrain the design._
_Generated by /opsx:propose council on 2026-10-07_

Sources are pinned as in `proposal.md`: the openxFactory packet
`openspec/changes/prefer-triad-project-shape/` at `e6380965` ("packet"),
openRepoShape at `1a9fc537`, workBenches at `9fbe609c` (the merged bootstrap
advisory, #139) and main `d86ba59` (current setup-openspeckit, #140), and
`project` at this change's head.

## Council-Identified Constraints

### The interactive double question under `--workflow`
**Raised by:** product-advocate / adversary-engineer (systems-architect raised
the same interaction)
**Concern:** With `--workflow`, `project` runs `setup-openspeckit --repo
<destination>` on the repository it has just created (`project:242-245`,
`:265-269`), and setup-openspeckit is a ratified advisory surface of its own.
Its `main()` calls `give_triad_advisory()` before its first write (workBenches
`d86ba59`, `setup-openspeckit:4075-4078`). At a terminal, a person who has just
answered `2` at `project`'s question is advised again and asked "Press Enter
to continue (s = show the steps; n or Ctrl-C stops before anything is
written)" (`setup-openspeckit:119-122`). Since `d86ba59`, `n` or `no` there
ends setup-openspeckit by SIGINT (`:2320-2356`), so `project` prints "Project
created at ...; workflow setup failed." and exits 130 (`project:162`,
`:266-269`), while at `project`'s own question `n` takes the single
repository. The packet's requirement is titled "A person starting work outside
a Triad is advised once, and is never stopped for it" (packet `spec.md:90`),
yet it ratifies both forms that meet here: the bootstrap "in an interactive
terminal SHALL ask before continuing, with continuing as the default answer"
(`spec.md:96`), and "the creating tool or agent SHALL offer the Triad first"
(`spec.md:99-100`). The double offer comes from chaining two ratified
surfaces, not from either one.
**Design impact:** The design chooses and pins the order of output under
`--workflow`. The proposal's ruling is that `project` gives its own offer (the
question, or the stderr advisory where D2 covers the creation) and then
setup-openspeckit's output follows; `project` neither suppresses it nor
coordinates with it. The design records, as known consequences, the second
question after an answer of `2`, the two advisories of `--yes --workflow`, and
the advisory setup-openspeckit gives a `-wip` creation unless the person's
`workspace.yaml` names it. One test with an echoing fake
`setup-openspeckit` pins the order. Real suppression is workBenches'
follow-up: an optional "already offered" signal that setup-openspeckit could
accept, carried in the handoff issue on opensoft/workBenches (proposal,
Impact, "Handoffs").

### Agents at a pseudo-terminal look like people
**Raised by:** product-advocate / adversary-engineer
**Concern:** The terminal helper (stdin and stdout are terminals, `CI`
false-y) cannot tell a person from an agent or script driving `project new`
through a pseudo-terminal, such as tmux `send-keys` or `expect`, and this
estate runs its lanes in tmux. Such a driver meets a new prompt between
`Project name:` and the confirmation, and Enter takes the Triad. The ratified
design rests the guard against an unconfirmed Triad on a confirmation an agent
does not answer, not on detection: "No surface creates three repositories on
a default: openRepoShape's `setup.sh` keeps its own typed confirmation, which
its AGENTS.md already makes an agent unable to answer on a person's behalf"
(packet `design.md:85-87`). The shared protocol gives an agent the
conversational form: "When a person asks to create a new project, offer the
Triad first as the default, accept a single repository without asking why,
and create nothing they have not confirmed."
**Design impact:** The design records the residual risk: an agent at a
pseudo-terminal is indistinguishable from a person, so for such a run the
guarantee that no repository is created unconfirmed rests on openRepoShape's
typed confirmation, today's `Type yes` on the bench path, and the agent
instructions. `AGENTS.md` and the README tell an agent to offer the Triad in
conversation and then to run `project new` with a choosing flag, never
answering the question on a person's behalf (proposal, Impact).

### Ctrl-C while openRepoShape runs
**Raised by:** adversary-engineer
**Concern:** `project` runs the openRepoShape launcher through `execute()`,
which is `subprocess.run` (`project:157-162`). On Ctrl-C, CPython's `Popen`
waits a further 0.25 seconds for the child (`_sigint_wait_secs = 0.25`) and
`run()` then kills it (`except: process.kill()`), before `project` prints
`Cancelled.` and exits 130 (`project:1001-1003`). The launcher is killed
outright, so its `trap 'rm -rf -- "$WORKDIR"' EXIT` (`openRepoShape:114`)
never runs and its temporary directory is left behind, and `setup-project.py`
and `gh` may still be running after `Cancelled.` is printed, by which point
repositories may exist. This is pre-existing `--shape` behaviour; making the
Triad the default answer makes it more common.
**Design impact:** The proposal's "An interrupt exits 130 with `Cancelled.`, as
today" covers the question and the two Triad prompts. The design decides, for
the time openRepoShape runs, whether `project` waits for its child (for
example, by ignoring SIGINT in the parent while the shape delegate runs, so
that openRepoShape handles the interrupt itself) or documents the behaviour as
it stands.

### Is the creation question owed for a `-wip` name?
**Raised by:** alignment review
**Concern:** The ratified text exempts a workspace repository from the
advisory: "a `<user>-wip` workspace repository, which holds no code and elects
nothing, identified by the naming family openRepoShape's naming policy
declares for it or because the person's own workspace configuration names it"
(packet `spec.md:186`), under "The advisory is silent where the shape question
is answered or does not arise" (`spec.md:178`). It is silent on the creation
question for such a name, which "Where a person creates a new project, the
creating tool or agent SHALL offer the Triad first" (`spec.md:99-100`) would
otherwise cover.
**Design impact:** The proposal reads the offer as not owed where the advisory
is not owed: a name matching `^[a-z0-9]+(?:-[a-z0-9]+)*-wip$` (openRepoShape
`contracts/repository-naming.yaml:412`, `1a9fc537`) gets neither the question
nor the stderr advisory, and only the name form is read at creation. The design
keeps that reading unless the doctrine owner (openxFactory, lane
codeXfactory-5) says otherwise.

### The prompt's stream differs between harnesses
**Raised by:** systems-architect
**Concern:** At a real terminal, CPython without readline writes an `input()`
prompt to stderr (bpo-1927), and a script never loads readline, so this holds
under `python3 -I` too. Every `ask()` prompt (`project:146-149`) therefore
lands on stderr at a terminal, while an in-process test that redirects stdout
sees the same prompt on stdout. The alignment review assumed the opposite
("`input()` writes its prompt to stdout").
**Design impact:** The design pins that the question's prompt goes through
`ask()`, so it behaves exactly as today's prompts do; the question's other
lines go to stdout. In-process `module.main()` tests patch `input` as well as
`isatty`. No test, pty-driven or in-process, asserts which stream a prompt
appears on. The pty helper is the one the proposal specifies (Impact, tests).

### Exit codes on the Triad path
**Raised by:** systems-architect / adversary-engineer
**Concern:** A known obstacle (the name outside the assembly-root form,
`openRepoShape` missing, the parent directory missing) is refused by
`project`. A refusal by openRepoShape arrives through `execute()`, which
returns the child's status (`project:162`, `:254-256`). openRepoShape's name
check exits 1 (`die(..., 1)`, `setup-project.py:1532-1535`); its `die()`
defaults to 2 (`setup-project.py:224-225`), and its launcher's own `die()`
defaults to 2 as well (`openRepoShape:53`).
**Design impact:** Known obstacles exit 2 from `project`, before any prompt
beyond the question and before openRepoShape runs. Anything openRepoShape
refuses exits with openRepoShape's own status, which is 1 for its name check.
The spec delta must not promise exit 2 for an openRepoShape refusal; its
scenarios for that case promise only that the status is passed through.
Loading
Loading