This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

A repository sets the layout limits of abcd's questions

specopenspc-2610301950037469

Summary

Delivers itd-2610301914350541, which builds on itd-2610230756259215. Three keys in a repository's .abcd/config.json, question.rows, question.label_words, and question.description_sentences, set the rows a question may take, the words a label may run to, and the sentences a description may say. One resolver in internal/core/layered reads them over question.Default, whose Rows rises from 24 to 32 in the same change (the intent's decision 3), lets the person's own ~/.abcd.noindex/config.json tighten a key and never loosen it (decision 5), holds both files to inclusive bounds stated once in internal/core/question, and returns one of three states: The defaults, the limits in force with the file that set each, or a refusal naming the key, the value, the bounds or the limit in force, and the file. Every reader of a limit takes it from that resolver: The question gate in the hook, the abcd guard question verb, the plain-Terminal interviews' checks, the install's README fit, and the GRILL domain the rules loader generates, which gains a LoadWith form the two front doors feed. A value outside its bounds, of the wrong type, under a key the namespace does not claim, or set in the machine's file looser than the limit in force is refused and nothing falls through: The hook refuses every question carrying abcd's header, the verb exits 2 with refused, and the asking rules are rendered at the defaults with the refusal written in. A file the resolver cannot read fails open and loud, in the gate's own NOT CHECKED form. The shipped page blocks keep stating abcd's defaults and point the writer at the check's new limits field.

This spec runs after spc-2610301917245442's verb exists. It extends that verb's abcd/question-check/v1 JSON with three additive fields, limits, limits_source, and refused, and adds one more cause of exit 2, a refused value, beside the parent's unreadable input and unread store; the parent spec's approach 5 and its step 1 are edited to list them in the change that lands step 2 here, so the verb's contract stays stated once (the intent's decision 15). Which parent steps each step here waits for is said under each step.

Grounding, read on 30 October 2026:

  • Every limit is one value, question.Default (internal/core/question/limits.go lines 42 to 67), with Rows 24 (32 after step 1), LabelWords 5, MeaningSentences 2, Columns 80, and the three measured host figures HostTextColumns 76, HostChromeRows 8, and HostOptionColumns 74 (lines 52 to 63, calibrated and not chosen). The check reads LabelWords and MeaningSentences in rules 4 and 5 (check.go lines 214 to 230) and Rows in rule 13 (lines 335 to 338); QuestionsPerCall (line 126) and OptionsPerQ (line 203) stay fixed. The row estimate is splitRows (rows.go lines 30 to 36), the frame plus the text wrapped at HostTextColumns plus each option at HostOptionColumns. The asking rules are AskingRules(l Limits) (asking.go lines 35 to 95); the layout rule (lines 51 to 61) states rows, label words, and sentences in words through numberWord (lines 142 to 151, words to ninety-nine).
  • The layered resolver (internal/core/layered/layered.go) is the one reader of .abcd/config.json and ~/.abcd.noindex/config.json (Config, line 120). RootsFor (lines 188 to 201) takes the rules loader's root, rules.Resolve (internal/core/rules/root.go line 145), not git's toplevel, so a monorepo member reads its own file. LoadContext (lines 278 to 322) reads the repository layer through readRepo (lines 335 to 352, an os.Root, a symlink or non-regular file refused at line 347) and the machine layer through readMachine (lines 391 to 413, the home-declaration guard: Behind a symlinked directory, writable by others, or foreign-owned refused, an absent file (nil, nil) at lines 398 to 400). Every load fault is a formatted error with no exported type, so a caller cannot tell an unreadable layer from a refused value without reading words. Claim (lines 678 to 691) refuses an unknown key naming the key, the file, and the keys the namespace takes; Get (lines 819 to 835) decodes strictly (Decode, lines 841 to 851, a fraction refused for an integer) and runs the caller's check, naming the origin, the key, and the value, and never falls through. InterviewList (interview.go lines 22 to 47) is the one key with a layer rule of its own: It refuses the repository layer naming the machine's file (lines 34 to 40); the question keys take a third shape, the machine layer admitted only where it tightens. layered imports rules (line 64), and ahoy, oracle, and interview already import layered; source does not, and layered reaches none of the four, so each composer may call the resolver.
  • rules.Load (internal/core/rules/rules.go lines 240 to 285) reads the user and repository layers, builds withRepoShellDomain(Defaults(), repoRoot) (line 252), merges, validates, and returns noteWithheld. withRepoShellDomain (shell.go lines 101 to 118) is the pattern for a domain regenerated per repository before any layer lands: It replaces rs.Domains[ShellDomain] and appends to rs.notes on a fault. The GRILL domain is grillDomain() (grill.go lines 25 to 31), question.AskingRules(question.Default) added to the parsed defaults at init by withGrillDomain (lines 37 to 46). Provenance is ResolvedDomain.Source (lines 150 to 154), set only by mergeFrom for a domain an override layer named, and Label (lines 1112 to 1120) prints the heading. Signature (lines 1123 to 1127) hashes the rendered block, and the dedup ledger compares it per domain (inject.go lines 150 and 164), so a domain whose text changes is injected again. The notes reach stderr through rs.Notes() (line 113).
  • The two front doors that load the rules are the prompt hook (internal/surface/cli/cli.go line 1865) and abcd rules (line 2646), each after rulesRoot (line 7046, rules.Resolve with its notes printed). abcd rules prints the loader's notes on stderr (lines 2652 to 2654) and renders rulesView (lines 2569 to 2572), whose domains carry source.
  • The hook's question gate is presentQuestionGate (internal/surface/cli/guard_question.go lines 157 to 238): It calls question.CheckLimits(fields, question.Default, ...) at line 195, and ChipRole(..., question.Default) at lines 251 and 345. questionFailOpen (lines 447 to 451) is the gate's fail-open line, abcd guard: NOT CHECKED, the reason, and The question runs UNGATED., exit 1, used for the mode store at line 171. The parent spec lifts the decision into question.Decide(f, s State, l Limits) and the store reads into questionState(cmd, cwd); this spec adds one read to questionState and three fields to the verdict.
  • cmd/asking-sync/main.go renders both page blocks from question.Default (line 171): render (lines 69 to 79) for commands/intent.md and renderDrafter (lines 86 to 101) for agents/question-drafter.md, the drafter's row count stating l.Columns, l.Rows, and the three host figures (lines 95 to 98). make asking-sync-check (Makefile lines 283 to 284) gates drift through TestIntentPageAskingBlockIsGenerated and TestDrafterBlockIsGenerated (main_test.go lines 47 and 291).
  • The binary's own composed questions are held to the defaults by TestEverySetupQuestionPassesTheLimits (internal/core/ahoy/setup_questions_limits_test.go line 66, every setup value and confirm question and the README pair, setupLimitsOwed empty at line 56), TestEveryCategoryApprovalPassesTheChecks (category_approval_test.go line 73), TestSessionsViewPassesTheLimits (sessions_ask_test.go line 204), TestConventionsRetireQuestionPassesTheLimits (conventions_test.go line 492), and TestGuideQuestionsPassTheAskingLimits (internal/core/oracle/connect_guide_test.go line 619, at least twelve questions under two addressees); the source question (internal/core/source/add.go lines 59 to 70) and the written interview's sample (internal/core/interview/written.go lines 573 to 585) have no such test. The longest labels are four words: setup_question.go line 184, category_approval.go line 35, connect_guide.go lines 455, 456, and 511. The longest descriptions are two sentences: connect_guide.go lines 455 and 511, and source/add.go lines 62 and 67.
  • Two composers measure at run time against question.Default: The install's README fit, fitsLimits (readme_ask.go lines 305 to 309) and fitLines (lines 332 to 340, bounded by Default.Rows), and the plain-Terminal written interview's checkAsk (written.go lines 207 to 210), whose brief also renders AskingRules(question.Default) (line 588). Written carries Verbs for the register rule (line 120) and nothing for the limits. The setup interview (setup_interview.go line 278) and the written interview's front door (written_interview.go line 186) draw at Default.Columns, a fixed figure.
  • The brief's configuration chapter (.abcd/development/brief/05-internals/03-configuration.md) documents .abcd/config.json key by key (lines 29 to 54) and interview.list under ### The interview key (lines 509 to 546), with its JSON block and the layer it is read from. The guard chapter (04-surfaces/17-guard.md) holds the machine-checked sub-verbs table (lines 29 to 34, surface_coverage against .abcd/development/release/surface.json, internal/core/lint/config.go lines 186 to 191), the exit rules under "What a person gets back" (lines 47 to 78), and the question gate section (from line 199, question.CheckLimits over question.Default at lines 216 to 217). commands/guard.md describes the question gate under hook (lines 98 to 142).
  • fsutil.ReadHomeDeclaration (internal/fsutil/home.go lines 550 to 575) rules that a file that is not there is DeclarationAbsent whatever the directories are, so a symlinked ~/.abcd.noindex holding no config.json reads as absent, and one that is there behind a symlinked directory is refused before a byte is read.

Approach

  1. The bounds, stated once, and the new default. Default.Rows becomes 32 (the intent's decision 3; the comment on it says the floor stays 24 and why the default is above it), and every test that asserts the rows limit as 24 or "twenty-four" in a rendered rule, a page block, or a verb result moves to 32, the parent's corpus fixtures included. internal/core/question/limits.go gains Bound{Key string; Floor, Ceiling int} and Bounds, three values beside Default: question.rows 24 to 60, question.label_words 4 to 8, and question.description_sentences 2 to 4 (decision 3). Bound.String() renders "24 to 60", Bound.Admits(n int) bool judges inclusively, and Bounded(l Limits, rows, labelWords, sentences int) Limits returns l with the three fields replaced and every other field unchanged. Floors(l) and Ceilings(l) are test helpers in the same file. Nothing else in the tree writes a bound: The resolver's refusal, the loader's note, and the generated page sentence all render Bounds.
  2. The resolver. layered.QuestionLimits(r Roots) (Value[question.Limits], error) in a new internal/core/layered/question.go, beside InterviewList. It loads Config, claims the question namespace with the three keys, so question.options, question.columns, question.tabs, or a misspelt key is refused by Claim naming the key and the file. It then looks each key up in both layers. Every value found, in either file, is decoded as an int through Decode, so a fraction, a string, or null is refused, and judged by its Bound; a value outside it is a *layered.KeyRefusedError{Key, Value, Bounds, Origin}, whose Error() is Get's form: .abcd/config.json (repo layer): question.rows is 100: outside 24 to 60. For each key the limit otherwise in force is the repository's value where set, else Default's; a machine value at or below it replaces it, and one above it is refused with the same type, its Limit and LimitOrigin fields set, in the form ~/.abcd.noindex/config.json (machine layer): question.rows is 40: looser than 32 (bundled); a personal value may only tighten (decision 5). The bounds are judged before the comparison, so a personal value below the floor is refused naming the bounds, never the limit. No key set returns Default with Layer Bundled and Origin bundled; otherwise Bounded(Default, ...) with, per key, the Layer and Origin of the file whose value is in force, which is what limits_source reports key by key; the Value's own Layer and Origin are the loosest layer that set anything, so a caller that wants one word still has one. The root is the one RootsFor resolves (the rules root), so the injected domain and the hook's check read one file; a front door that has already resolved that root through rulesRoot builds the same Roots from HomeRoots and that root, so git is asked once.
  3. An unreadable layer is told apart from a refused value. LoadContext wraps every fault of a layer it could not read or parse, the repository read (lines 298 to 304), the machine read (lines 313 to 319), and the unresolved home (lines 283 to 285), in *layered.UnreadError{Origin string; Err error}, whose Error() is the text today, so no caller's message changes. Everything after a successful load, Claim's unknown key and KeyRefusedError in both its forms (outside the bounds, or a personal value that loosens), is a refusal. A caller tells the two apart with errors.As and never by reading words (decision 8). The hook, the verb, and the binary's composers take two paths from them: A refusal is fail-closed (the question is not asked), and an unread layer is fail-open and loud (the question runs at the defaults, and one line names the file).
  4. The check reports and the hook reads. questionState (the parent's one store reader) resolves the limits through approach 2 and puts the outcome on question.State: Limits, LimitsSource (one origin per key), Refused *LimitsRefusal{Key, Value, Bounds, Limit, LimitFile, File}, or an unread entry naming the file and the three rules the keys decide. Decide keeps the parent's order, away first, then the header test, then the limits: A call with no header in abcd's grammar is admit-unchecked whatever the configuration says; a call in the grammar with Refused set is refuse with findings empty and Verdict.Refused carrying the refusal; one with the layer unread is checked at the defaults, partial with that unread entry, so an admission it would otherwise make silently is unchecked (approach 3). The hook denies a refuse with Refused by one head line and the refusal line, naming the key, the value, the bounds (or, for a loosening personal value, the limit in force and the file that set it), and the file and no asking rule, then the parent's pointer line; with a layer unread it writes one NOT CHECKED line naming the file and saying the question is checked at the defaults, at the head of a deny's reason or a rows note, and otherwise on stderr at exit 1. Every admitted question is checked with State.Limits, so a tab of 30 rows under question.rows 40 gets no rows note and one of 41 gets the note naming 40. The limits reach ChipRole and the parent's IsAbcds the same way. The mode store keeps its own root (mode.Root, git's toplevel); the limits follow the rules root, as decision 6 says.
  5. The verb's JSON. abcd/question-check/v1 gains limits (rows, label_words, description_sentences), limits_source (the same three keys, each bundled, .abcd/config.json, or ~/.abcd.noindex/config.json), and refused (key, value, bounds, file, and for a loosening personal value limit and limit_file) on the refusal path; rows_limit stays and equals limits.rows. Exit 2 for a refused value, with no ready, and exit 2 for an unread layer, the parent's partial result naming the file. The text render prints one line after the outcome: limits: 40 rows (.abcd/config.json), 5 label words, 2 description sentences, a file named after each figure a file set and nothing after a default, or the refusal line. The parent spec's approach 5 and step 1 are edited in the same change to list the three fields and the new exit-2 cause, as decision 15 requires.
  6. The binary's own composers. Written gains Limits question.Limits and LimitsNote question.LimitsNote, set by its front door from the resolver; checkAsk measures with w.Limits, and the brief renders AskingRulesFrom(w.Limits, w.LimitsNote) (approach 7). ahoy install resolves once before its first question and threads the limits to fitsLimits and fitLines, which lose their Default reads. In both, a refusal is the run's refusal before any question, exit 2 naming the key, the value, the bounds, and the file, and an unread layer prints the gate's fail-open line and goes on at the defaults. The composers that only build fixed questions (setup, connect guide, source, sessions) read Default.LaterLabels, Default.NotApplicable, and the prefixes, which stay the binary's (decision 2), and are held at the floors by tests (approach 10). A tree test lists the call sites still allowed to pass question.Default to CheckLimits, AskingRules, AskingRulesFrom, ChipRole, IsAbcds, Rows, or the asking-sync renderers, or to read Default.Rows, Default.LabelWords, or Default.MeaningSentences: The resolver, cmd/asking-sync/main.go, the rules package's one bundled-layer statement (the init-time GRILL in grill.go, which rules.Load's one-line body delegates through), and tests.
  7. The asking rules carry the source. question.LimitsNote{SetBy string; Refused string} and AskingRulesFrom(l Limits, n LimitsNote) []string; AskingRules(l) becomes AskingRulesFrom(l, LimitsNote{}) and renders byte for byte as today. With SetBy set (one origin per key, as limits_source carries them), the layout rule ends with one sentence naming .abcd/config.json as having set the figures it set, and the person's own ~/.abcd.noindex/config.json as having tightened any it set, key by key. With Refused set, it says that the figures stated are abcd's defaults, which key is refused and why, and that abcd's question check refuses every abcd question until the file is fixed. A new sentence in the same rule, rendered in every repository from Bounds, says the three keys a repository may set, their bounds, and that a person's own file may set them lower and never higher. The parent's step 4 revises the same rules; this step lands after it, so the two never edit one sentence at once.
  8. The loader takes the limits as an argument. rules.LoadWith(repoRoot string, l question.Limits, n question.LimitsNote) (RuleSet, error) regenerates rs.Domains[GrillDomain] on the base, after withRepoShellDomain and before any layer lands, from AskingRulesFrom(l, n); a non-empty n.Refused is appended to rs.notes as one line, so abcd rules prints it on stderr and the hook's diagnostic carries it. rules.Load(repoRoot) is LoadWith(repoRoot, question.Default, question.LimitsNote{}), the form every test and every caller outside a repository keeps. The domain's Source stays bundled, because no override layer named it, and rulesFrom is left unset: Setting a figure is not replacing the rules (decision 11). A repository that overrides GRILL's rules through .abcd/rules.json still wins, since the layers land after the regeneration. The changed text changes Signature, so the dedup ledger injects the domain again on the next prompt that recalls GRILL, with no new mechanism.
  9. The two front doors. The prompt hook (cli.go line 1865) and abcd rules (line 2646) resolve the limits before loading the rules: In force, LoadWith(root, l, LimitsNote{SetBy: origins}) with SetBy empty when every key is bundled; refused, LoadWith(root, question.Default, LimitsNote{Refused: err.Error()}); unread, LoadWith(root, question.Default, LimitsNote{}) with one diagnostic line on stderr naming the file in the resolver's words. So every other domain loads as before, and a configuration fault never injects nothing.
  10. The pages, the brief, and the labs. render and renderDrafter keep rendering from question.Default (decision 12) and each gains one sentence, composed in question from Bounds: These figures are abcd's defaults; a repository sets rows, label words, and description sentences in .abcd/config.json under question, within the bounds named, and a person's own file may only set them lower; abcd guard question reports the values in force in its limits field, and the writer takes them from there and never from either file. The writer's hand-written part says that a hand count, made only where the host gives it no shell, is made at abcd's defaults and that neither the repository's values nor the person's were read, and its output says so (decision 10). The configuration chapter gains ### The question keys after the interview key, with a JSON block naming the three keys, their defaults (rows 32), their bounds, and the layering: The repository sets a key, and ~/.abcd.noindex/config.json may set it lower and is refused where it sets it higher (decision 5); the guard chapter's question row and "What a person gets back" name the exit 2 for a refused value, and its question gate section says the limits are the resolver's; commands/guard.md's question section says where the limits are set and that the check reports them. One lab, question-writer-rows-40, on the parent's pinned binary and a managed snapshot repository whose .abcd/config.json sets question.rows to 40: Three recorded runs of the writer on the parent's 25-row material, each transcript read for one returned part the verb passes at 40 rows, the limits the writer reports, and any read of .abcd/config.json; the harvest is captured into the parent's research note.

How each criterion is met

  • The resolver returns Default with the three fields replaced; the hook and the verb pass it to CheckLimits. Approaches 1, 2, and 4. Tests: TestQuestionLimitsReplaceThreeFieldsOnly in internal/core/layered/question_test.go (a fixture with all three keys; every other field equals Default's); TestGuardQuestionChecksWithTheRepositorysLimits in internal/surface/cli/guard_question_verb_test.go (a 30-row tab under rows 40 has no rows finding; 41 has one naming 40); TestHookAdmitsThirtyRowsUnderFortyAndNotesFortyOne in guard_question_rows_test.go.
  • An unclaimed key is refused naming the key and the file; nothing falls through. Approach 2. Test: TestQuestionLimitsRefuseAnUnknownKey (question.options, question.columns, question.tabs; the error names each and .abcd/config.json, and no value is returned).
  • A personal value at or below the limit in force applies and is reported as the machine file's. Approach 2. Test: TestQuestionLimitsLetThePersonTighten in internal/core/layered/question_test.go, three fixtures: Rows 24 in the machine file under a repository that set 40 (24 in force, limits_source.rows the machine file); rows 24 under a repository that set nothing (24 in force over the default 32); and a machine value equal to the repository's (the repository's value in force, reported as the machine file's, since equal is at the limit).
  • A personal value above the limit in force is refused naming the key, the personal value, the limit in force, and the file that set each; nothing falls through. Approach 2. Test: TestQuestionLimitsRefuseAPersonWhoLoosens, two fixtures: Rows 40 in the machine file under a repository that set 32 (refused naming 32 and .abcd/config.json), and rows 40 under a repository that set nothing (refused naming 32 and bundled); each asserts KeyRefusedError's Limit and LimitOrigin, that no value is returned, and that the repository's own value is not the one in force either.
  • The default rows is 32, and the floor stays 24. Approach 1. Test: TestDefaultRowsIsThirtyTwoAboveTheFloor in internal/core/question/limits_test.go (Default.Rows is 32, Bounds for question.rows is 24 to 60, and AskingRules(Default) states thirty-two).
  • Floor and ceiling accepted; below, above, and a non-integer refused naming key, value, and bounds. Approaches 1 and 2. Test: TestQuestionLimitsHoldTheBounds, a table over the three keys, five values each, and both files, asserting KeyRefusedError's fields (a personal value below the floor is refused naming the bounds, not the limit) and that Bounds is the text's source (TestBoundsAreStatedOnce in internal/core/question/limits_test.go greps the resolver, the loader, and the renderers for no literal bound).
  • The hook's deny names key, value, bounds, and file and no rule; a question not in the grammar runs unchecked. Approach 4. Tests: TestHookDenyNamesTheRefusedKeyAndNoRule and TestUnlabelledQuestionRunsUncheckedUnderARefusedValue in guard_question_limits_test.go; a refused-value fixture is added to testdata/questions/corpus/, so TestGuardQuestionAndHookDecideAlike holds the verb and the hook to one answer.
  • The verb exits 2 with refused and no ready. Approach 5. Test: TestGuardQuestionExitsTwoOnARefusedValue (any draft, clean included).
  • The GRILL domain under a refused value states the defaults, names the key, says the check refuses, the notes carry the line, abcd rules prints it, every other domain loads. Approaches 7 to 9. Tests: TestLoadWithARefusalWritesItIntoGrillAndTheNotes in internal/core/rules/grill_test.go; TestRulesPrintsTheRefusedLimitOnStderr in internal/surface/cli/rules_grill_domain_test.go (stdout still renders every bundled domain).
  • In force, the layout rule states the figures in words and names the file; unset, it reads as today. Approach 7. Tests: TestAskingRulesFromNamesTheFileThatSetTheLimits and TestAskingRulesWithNoNoteAreUnchanged (byte equality with AskingRules(Default)) in internal/core/question/asking_test.go.
  • source stays bundled and the heading carries no label. Approach 8. Test: TestConfiguredRepositoryKeepsGrillBundled in rules_grill_domain_test.go, beside TestAbcdsOwnRulesReportGrillBundled.
  • limits, limits_source, rows_limit, and the injected figures agree from one fixture. Approaches 5, 8, and 9 (the Mechanism claim). Test: TestCheckAndGrillStateOneLimits in internal/surface/cli/guard_question_verb_test.go renders the verb's JSON and the prompt hook's injection from one fixture checkout with rows 40, label words 6, and sentences 3, and asserts the three figures in limits are the words the layout rule states, rows_limit equals limits.rows, and every entry of limits_source is .abcd/config.json; a second fixture adds a machine file setting rows 30 and asserts both render 30, limits_source.rows is the machine file, and the other two entries stay the repository's.
  • A changed value re-injects the domain. Approach 8. Test: TestChangedLimitsReinjectGrill in internal/core/rules/inject_test.go (a ledger holding the old signature, a second load at new figures, the domain injected again).
  • The generated blocks state the defaults, the keys and bounds, and the check's limits field; the planning page carries the same sentence. Approach 10. Tests: TestDrafterBlockPointsAtTheChecksLimits and TestIntentPageBlockCarriesTheSameLimitsSentence in cmd/asking-sync/main_test.go; TestDrafterBlockIsGenerated and TestIntentPageAskingBlockIsGenerated hold the committed pages.
  • Three lab runs at rows 40 return one part, report limits, and read no file. Approach 10. The lab's INTENTION.md states the pass rules before any run; each transcript is read for the verb calls, the returned limits, and any read of .abcd/config.json, and the criterion passes only when all three runs pass.
  • A writer with no shell says it counted at the defaults and read neither the repository's values nor the person's. Approach 10. Test: TestWriterPageStatesItsRefusalsAndFallbacks in internal/core/lint extended to the two sentences.
  • Every floor and ceiling combination admits the minimal question and refuses the four broken forms. Approach 1. Test: TestEveryBoundCombinationKeepsTheFixedRules in internal/core/question/limits_test.go, eight combinations, one admitted question and four refused ones each.
  • Every composed question is admitted whole at the floors. Approaches 1 and 6. Tests, each beside the one that holds its questions to the defaults: TestEverySetupQuestionPassesTheLimitsAtTheFloors (setup_questions_limits_test.go), TestEveryCategoryApprovalPassesTheChecksAtTheFloors, TestSessionsViewPassesTheLimitsAtTheFloors, TestConventionsRetireQuestionPassesTheLimitsAtTheFloors, TestGuideQuestionsPassTheAskingLimitsAtTheFloors (connect_guide_test.go), TestSourceQuestionPassesTheLimitsAtTheFloors (new, internal/core/source/add_test.go), and TestWrittenSampleQuestionPassesTheLimitsAtTheFloors (new, internal/core/interview/written_test.go). Each measures with question.Floors(question.Default).
  • An unreadable .abcd/config.json is loud and leaves the question checked at the defaults; the verb exits 2 naming it. Approaches 3 and 4. Tests: TestHookChecksAtTheDefaultsOnASymlinkedConfig and TestHookChecksAtTheDefaultsOnAnUnparseableConfig in guard_question_limits_test.go (a well-built question runs at exit 1 with the NOT CHECKED line naming the file, one that breaks a rule is refused on it, and a rows note names the default), TestHookChecksAtTheDefaultsWithHomeUnset for an unresolved home, and TestGuardQuestionExitsTwoNamingAnUnreadConfig (partial true, unread naming the file, findings at the defaults).
  • An unreadable ~/.abcd.noindex/config.json with no question key fails open and names the file; a symlinked ~/.abcd.noindex with no file is silent. Approach 3. Test: TestQuestionLimitsAndTheSymlinkedHome in internal/core/layered/question_test.go, laid out both ways after home_link_test.go's pattern, and TestHookNamesAnUnreadMachineConfig in guard_question_limits_test.go.
  • A five-tab call is refused on the tabs rule at every ceiling. Approach 1. Test: TestTabsStayFixedAtTheCeilings in limits_test.go.
  • Only the resolver, asking-sync, the bundled layer, and tests pass Default. Approach 6. Test: TestOnlyTheResolverPassesTheDefaultLimits in internal/surface/cli/limits_callsites_test.go, a tree walk over non-test Go files with the allowlist by path, failing on any other question.Default passed to the named functions or any read of its three configurable fields.
  • The bounds are stated once. Approach 1. TestBoundsAreStatedOnce above.
  • Brief, page, and gates. Approach 10. Tests: TestConfigurationChapterListsTheQuestionKeys in internal/core/lint (the three keys, defaults, and bounds under question); the surface_coverage lint over the guard chapter's table (the question row's bucket and status unchanged, the exit 2 named in prose); TestGuardPageDocumentsTheQuestionVerb extended to the limits sentence; docs-lint and record-lint under preflight.
  • Seen by a person: No tab cut at 40 rows; the fixed rules hold in every repository; a refusal before any question; the check by hand states the values and their source; an unconfigured repository is unchanged but for the rows default; a person who tightened reads a question that fits their window while a peer reads it at the repository's. Approaches 2, 4, 5, and 10. The first is the lab; the second is TestEveryBoundCombinationKeepsTheFixedRules; the third is TestGuardQuestionExitsTwoOnARefusedValue with TestHookDenyNamesTheRefusedKeyAndNoRule and the install's TestInstallRefusesBeforeItsFirstQuestionOnARefusedValue in internal/core/ahoy; the fourth is the verb's text render test TestGuardQuestionTextStatesTheLimitsInForce; the fifth is TestAskingRulesWithNoNoteAreUnchanged with the parent's corpus test, whose fixtures set no value and whose expectations change only where they name the rows limit, 24 to 32; the sixth is TestHookJudgesAtThePersonsTighterRows in guard_question_rows_test.go (a 30-row tab under a repository's 40 and a machine file's 24 gets the rows note naming 24; the same tab with no machine file gets none).

Out of scope, recorded elsewhere: A configurable options count (question.options, the intent's decision 16); configurable columns or host measures; a personal value that loosens a limit (decision 5); configurable tabs, header width, prefixes, or labels (decision 2); a verb or an install question that writes the keys, and a lint rule for them (decision 13, settled on 2026-10-31); a question.rows floor of 18, which the product thinker ruled against on 2026-10-31 (decision 17); re-calibrating the host's frame; and everything itd-2610230756259215 keeps out of its own scope.

Footprint

  • packages: internal/core/question/limits.go, internal/core/question/asking.go, internal/core/question/decide.go, internal/core/layered/layered.go, internal/core/layered/question.go, internal/core/rules/rules.go, internal/core/rules/grill.go, internal/core/interview/written.go, internal/core/ahoy/readme_ask.go, internal/core/ahoy/install.go, internal/surface/cli/guard_question.go, internal/surface/cli/guard_question_verb.go, internal/surface/cli/cli.go, internal/surface/cli/written_interview.go, internal/surface/cli/setup_interview.go, cmd/asking-sync/main.go, agents/question-drafter.md, commands/intent.md, commands/guard.md, .abcd/development/brief/05-internals/03-configuration.md, .abcd/development/brief/04-surfaces/17-guard.md, .abcd/development/specs/open/spc-2610301917245442-questions-still-break-abcd-s-asking.md, .abcd/development/intents/planned/itd-2610230756259215-questions-still-break-abcd-s-asking.md, .abcd/development/intents/shipped/itd-2610230756259215-questions-still-break-abcd-s-asking.md, .abcd/development/research/notes/2026-11-question-writer-labs.md
  • tests: internal/core/question/limits_test.go, internal/core/question/asking_test.go, internal/core/layered/question_test.go, internal/core/rules/grill_test.go, internal/core/rules/inject_test.go, internal/core/ahoy/setup_questions_limits_test.go, internal/core/ahoy/category_approval_test.go, internal/core/ahoy/sessions_ask_test.go, internal/core/ahoy/conventions_test.go, internal/core/ahoy/install_test.go, internal/core/oracle/connect_guide_test.go, internal/core/source/add_test.go, internal/core/interview/written_test.go, internal/surface/cli/guard_question_verb_test.go, internal/surface/cli/guard_question_limits_test.go, internal/surface/cli/guard_question_rows_test.go, internal/surface/cli/guard_question_corpus_test.go, internal/surface/cli/testdata/questions/corpus/, internal/surface/cli/rules_grill_domain_test.go, internal/surface/cli/limits_callsites_test.go, internal/core/lint/surfaceagents_test.go, cmd/asking-sync/main_test.go

Steps

  1. The new rows default, the bounds, the resolver, and the binary's own composers
    • needs: none
    • packages: internal/core/question/limits.go, internal/core/layered/layered.go, internal/core/layered/question.go, internal/core/interview/written.go, internal/core/ahoy/readme_ask.go, internal/core/ahoy/install.go, internal/surface/cli/written_interview.go, internal/surface/cli/setup_interview.go, .abcd/development/brief/05-internals/03-configuration.md, .abcd/development/intents/planned/itd-2610230756259215-questions-still-break-abcd-s-asking.md, .abcd/development/intents/shipped/itd-2610230756259215-questions-still-break-abcd-s-asking.md, .abcd/development/specs/open/spc-2610301917245442-questions-still-break-abcd-s-asking.md
    • tests: internal/core/question/limits_test.go, internal/core/layered/question_test.go, internal/core/ahoy/setup_questions_limits_test.go, internal/core/ahoy/category_approval_test.go, internal/core/ahoy/sessions_ask_test.go, internal/core/ahoy/conventions_test.go, internal/core/ahoy/install_test.go, internal/core/oracle/connect_guide_test.go, internal/core/source/add_test.go, internal/core/interview/written_test.go

Waits for no step of spc-2610301917245442: The default, the bounds, the resolver, the plain-Terminal interview's check, and the install's README fit read nothing the verb adds. The parent's lines that state 24 as the limit in force (its press-release example, its out-of-scope line, and its lab pass rule) are edited in this step's change, since the figure moves here. - landed: ef06f3cdc866

  1. The check reports and the hook reads
    • needs: 1
    • packages: internal/core/question/decide.go, internal/surface/cli/guard_question.go, internal/surface/cli/guard_question_verb.go, commands/guard.md, .abcd/development/brief/04-surfaces/17-guard.md, .abcd/development/specs/open/spc-2610301917245442-questions-still-break-abcd-s-asking.md
    • tests: internal/surface/cli/guard_question_verb_test.go, internal/surface/cli/guard_question_limits_test.go, internal/surface/cli/guard_question_rows_test.go, internal/surface/cli/guard_question_corpus_test.go, internal/surface/cli/testdata/questions/corpus/, internal/surface/cli/limits_callsites_test.go

Waits for spc-2610301917245442's step 1 (Decide, questionState, and the verb) and step 3 (the header grammar and IsAbcds), whose State and Verdict this step extends and whose exit codes it adds one cause to. - landed: 580ae5e5356a

  1. The asking rules, the loader, and the two front doors
    • needs: 2
    • packages: internal/core/question/asking.go, internal/core/rules/rules.go, internal/core/rules/grill.go, internal/surface/cli/cli.go
    • tests: internal/core/question/asking_test.go, internal/core/rules/grill_test.go, internal/core/rules/inject_test.go, internal/surface/cli/rules_grill_domain_test.go, internal/surface/cli/guard_question_verb_test.go

Waits for spc-2610301917245442's step 4, which revises the same asking rules, so the layout rule is edited once by each and never by both at once; the one-fixture test that holds the verb's limits to the injected figures needs step 2.

  1. The pages, the writer, and the lab
    • needs: 3
    • packages: cmd/asking-sync/main.go, agents/question-drafter.md, commands/intent.md, .abcd/development/research/notes/2026-11-question-writer-labs.md
    • tests: cmd/asking-sync/main_test.go, internal/core/lint/surfaceagents_test.go

Waits for spc-2610301917245442's step 4 (the writer's contract and the regenerated pages) and step 5 (the labs' pinned binary and snapshot repository), which this lab reuses with one configured value.

Frontmatter

idspc-2610301950037469
typespec
lifecycleopen

References1

mentions5