Development record
A repository sets the layout limits of abcd's questions
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.golines 42 to 67), withRows24 (32 after step 1),LabelWords5,MeaningSentences2,Columns80, and the three measured host figuresHostTextColumns76,HostChromeRows8, andHostOptionColumns74 (lines 52 to 63, calibrated and not chosen). The check readsLabelWordsandMeaningSentencesin rules 4 and 5 (check.golines 214 to 230) andRowsin rule 13 (lines 335 to 338);QuestionsPerCall(line 126) andOptionsPerQ(line 203) stay fixed. The row estimate issplitRows(rows.golines 30 to 36), the frame plus the text wrapped atHostTextColumnsplus each option atHostOptionColumns. The asking rules areAskingRules(l Limits)(asking.golines 35 to 95); the layout rule (lines 51 to 61) states rows, label words, and sentences in words throughnumberWord(lines 142 to 151, words to ninety-nine). - The layered resolver (
internal/core/layered/layered.go) is the one reader of.abcd/config.jsonand~/.abcd.noindex/config.json(Config, line 120).RootsFor(lines 188 to 201) takes the rules loader's root,rules.Resolve(internal/core/rules/root.goline 145), not git's toplevel, so a monorepo member reads its own file.LoadContext(lines 278 to 322) reads the repository layer throughreadRepo(lines 335 to 352, anos.Root, a symlink or non-regular file refused at line 347) and the machine layer throughreadMachine(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.golines 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.layeredimportsrules(line 64), andahoy,oracle, andinterviewalready importlayered;sourcedoes not, andlayeredreaches none of the four, so each composer may call the resolver. rules.Load(internal/core/rules/rules.golines 240 to 285) reads the user and repository layers, buildswithRepoShellDomain(Defaults(), repoRoot)(line 252), merges, validates, and returnsnoteWithheld.withRepoShellDomain(shell.golines 101 to 118) is the pattern for a domain regenerated per repository before any layer lands: It replacesrs.Domains[ShellDomain]and appends tors.noteson a fault. The GRILL domain isgrillDomain()(grill.golines 25 to 31),question.AskingRules(question.Default)added to the parsed defaults at init bywithGrillDomain(lines 37 to 46). Provenance isResolvedDomain.Source(lines 150 to 154), set only bymergeFromfor a domain an override layer named, andLabel(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.golines 150 and 164), so a domain whose text changes is injected again. The notes reach stderr throughrs.Notes()(line 113).- The two front doors that load the rules are the prompt hook (
internal/surface/cli/cli.goline 1865) andabcd rules(line 2646), each afterrulesRoot(line 7046,rules.Resolvewith its notes printed).abcd rulesprints the loader's notes on stderr (lines 2652 to 2654) and rendersrulesView(lines 2569 to 2572), whose domains carrysource. - The hook's question gate is
presentQuestionGate(internal/surface/cli/guard_question.golines 157 to 238): It callsquestion.CheckLimits(fields, question.Default, ...)at line 195, andChipRole(..., 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, andThe question runs UNGATED., exit 1, used for the mode store at line 171. The parent spec lifts the decision intoquestion.Decide(f, s State, l Limits)and the store reads intoquestionState(cmd, cwd); this spec adds one read toquestionStateand three fields to the verdict. cmd/asking-sync/main.gorenders both page blocks fromquestion.Default(line 171):render(lines 69 to 79) forcommands/intent.mdandrenderDrafter(lines 86 to 101) foragents/question-drafter.md, the drafter's row count statingl.Columns,l.Rows, and the three host figures (lines 95 to 98).make asking-sync-check(Makefile lines 283 to 284) gates drift throughTestIntentPageAskingBlockIsGeneratedandTestDrafterBlockIsGenerated(main_test.golines 47 and 291).- The binary's own composed questions are held to the defaults by
TestEverySetupQuestionPassesTheLimits(internal/core/ahoy/setup_questions_limits_test.goline 66, every setup value and confirm question and the README pair,setupLimitsOwedempty at line 56),TestEveryCategoryApprovalPassesTheChecks(category_approval_test.goline 73),TestSessionsViewPassesTheLimits(sessions_ask_test.goline 204),TestConventionsRetireQuestionPassesTheLimits(conventions_test.goline 492), andTestGuideQuestionsPassTheAskingLimits(internal/core/oracle/connect_guide_test.goline 619, at least twelve questions under two addressees); the source question (internal/core/source/add.golines 59 to 70) and the written interview's sample (internal/core/interview/written.golines 573 to 585) have no such test. The longest labels are four words:setup_question.goline 184,category_approval.goline 35,connect_guide.golines 455, 456, and 511. The longest descriptions are two sentences:connect_guide.golines 455 and 511, andsource/add.golines 62 and 67. - Two composers measure at run time against
question.Default: The install's README fit,fitsLimits(readme_ask.golines 305 to 309) andfitLines(lines 332 to 340, bounded byDefault.Rows), and the plain-Terminal written interview'scheckAsk(written.golines 207 to 210), whose brief also rendersAskingRules(question.Default)(line 588).WrittencarriesVerbsfor the register rule (line 120) and nothing for the limits. The setup interview (setup_interview.goline 278) and the written interview's front door (written_interview.goline 186) draw atDefault.Columns, a fixed figure. - The brief's configuration chapter (
.abcd/development/brief/05-internals/03-configuration.md) documents.abcd/config.jsonkey by key (lines 29 to 54) andinterview.listunder### 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_coverageagainst.abcd/development/release/surface.json,internal/core/lint/config.golines 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.CheckLimitsoverquestion.Defaultat lines 216 to 217).commands/guard.mddescribes the question gate underhook(lines 98 to 142). fsutil.ReadHomeDeclaration(internal/fsutil/home.golines 550 to 575) rules that a file that is not there isDeclarationAbsentwhatever the directories are, so a symlinked~/.abcd.noindexholding noconfig.jsonreads as absent, and one that is there behind a symlinked directory is refused before a byte is read.
Approach
- The bounds, stated once, and the new default.
Default.Rowsbecomes 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.gogainsBound{Key string; Floor, Ceiling int}andBounds, three values besideDefault:question.rows24 to 60,question.label_words4 to 8, andquestion.description_sentences2 to 4 (decision 3).Bound.String()renders "24 to 60",Bound.Admits(n int) booljudges inclusively, andBounded(l Limits, rows, labelWords, sentences int) Limitsreturnslwith the three fields replaced and every other field unchanged.Floors(l)andCeilings(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 renderBounds. - The resolver.
layered.QuestionLimits(r Roots) (Value[question.Limits], error)in a newinternal/core/layered/question.go, besideInterviewList. It loadsConfig, claims thequestionnamespace with the three keys, soquestion.options,question.columns,question.tabs, or a misspelt key is refused byClaimnaming the key and the file. It then looks each key up in both layers. Every value found, in either file, is decoded as anintthroughDecode, so a fraction, a string, ornullis refused, and judged by itsBound; a value outside it is a*layered.KeyRefusedError{Key, Value, Bounds, Origin}, whoseError()isGet'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, elseDefault's; a machine value at or below it replaces it, and one above it is refused with the same type, itsLimitandLimitOriginfields 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 returnsDefaultwithLayerBundledandOriginbundled; otherwiseBounded(Default, ...)with, per key, theLayerandOriginof the file whose value is in force, which is whatlimits_sourcereports key by key; theValue's ownLayerandOriginare the loosest layer that set anything, so a caller that wants one word still has one. The root is the oneRootsForresolves (the rules root), so the injected domain and the hook's check read one file; a front door that has already resolved that root throughrulesRootbuilds the sameRootsfromHomeRootsand that root, so git is asked once. - An unreadable layer is told apart from a refused value.
LoadContextwraps 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}, whoseError()is the text today, so no caller's message changes. Everything after a successful load,Claim's unknown key andKeyRefusedErrorin both its forms (outside the bounds, or a personal value that loosens), is a refusal. A caller tells the two apart witherrors.Asand 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). - The check reports and the hook reads.
questionState(the parent's one store reader) resolves the limits through approach 2 and puts the outcome onquestion.State:Limits,LimitsSource(one origin per key),Refused *LimitsRefusal{Key, Value, Bounds, Limit, LimitFile, File}, or anunreadentry naming the file and the three rules the keys decide.Decidekeeps the parent's order, away first, then the header test, then the limits: A call with no header in abcd's grammar isadmit-uncheckedwhatever the configuration says; a call in the grammar withRefusedset isrefusewithfindingsempty andVerdict.Refusedcarrying the refusal; one with the layer unread is checked at the defaults,partialwith thatunreadentry, so an admission it would otherwise make silently isunchecked(approach 3). The hook denies arefusewithRefusedby 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 oneNOT CHECKEDline 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 withState.Limits, so a tab of 30 rows underquestion.rows40 gets no rows note and one of 41 gets the note naming 40. The limits reachChipRoleand the parent'sIsAbcdsthe same way. The mode store keeps its own root (mode.Root, git's toplevel); the limits follow the rules root, as decision 6 says. - The verb's JSON.
abcd/question-check/v1gainslimits(rows,label_words,description_sentences),limits_source(the same three keys, eachbundled,.abcd/config.json, or~/.abcd.noindex/config.json), andrefused(key,value,bounds,file, and for a loosening personal valuelimitandlimit_file) on the refusal path;rows_limitstays and equalslimits.rows. Exit 2 for a refused value, with noready, 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. - The binary's own composers.
WrittengainsLimits question.LimitsandLimitsNote question.LimitsNote, set by its front door from the resolver;checkAskmeasures withw.Limits, and the brief rendersAskingRulesFrom(w.Limits, w.LimitsNote)(approach 7).ahoy installresolves once before its first question and threads the limits tofitsLimitsandfitLines, which lose theirDefaultreads. 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) readDefault.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 passquestion.DefaulttoCheckLimits,AskingRules,AskingRulesFrom,ChipRole,IsAbcds,Rows, or the asking-sync renderers, or to readDefault.Rows,Default.LabelWords, orDefault.MeaningSentences: The resolver,cmd/asking-sync/main.go, the rules package's one bundled-layer statement (the init-time GRILL ingrill.go, whichrules.Load's one-line body delegates through), and tests. - The asking rules carry the source.
question.LimitsNote{SetBy string; Refused string}andAskingRulesFrom(l Limits, n LimitsNote) []string;AskingRules(l)becomesAskingRulesFrom(l, LimitsNote{})and renders byte for byte as today. WithSetByset (one origin per key, aslimits_sourcecarries them), the layout rule ends with one sentence naming.abcd/config.jsonas having set the figures it set, and the person's own~/.abcd.noindex/config.jsonas having tightened any it set, key by key. WithRefusedset, 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 fromBounds, 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. - The loader takes the limits as an argument.
rules.LoadWith(repoRoot string, l question.Limits, n question.LimitsNote) (RuleSet, error)regeneratesrs.Domains[GrillDomain]on the base, afterwithRepoShellDomainand before any layer lands, fromAskingRulesFrom(l, n); a non-emptyn.Refusedis appended tors.notesas one line, soabcd rulesprints it on stderr and the hook's diagnostic carries it.rules.Load(repoRoot)isLoadWith(repoRoot, question.Default, question.LimitsNote{}), the form every test and every caller outside a repository keeps. The domain'sSourcestaysbundled, because no override layer named it, andrulesFromis left unset: Setting a figure is not replacing the rules (decision 11). A repository that overrides GRILL's rules through.abcd/rules.jsonstill wins, since the layers land after the regeneration. The changed text changesSignature, so the dedup ledger injects the domain again on the next prompt that recalls GRILL, with no new mechanism. - 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})withSetByempty when every key isbundled; 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. - The pages, the brief, and the labs.
renderandrenderDrafterkeep rendering fromquestion.Default(decision 12) and each gains one sentence, composed inquestionfromBounds: These figures are abcd's defaults; a repository sets rows, label words, and description sentences in.abcd/config.jsonunderquestion, within the bounds named, and a person's own file may only set them lower;abcd guard questionreports the values in force in itslimitsfield, 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 keysafter 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.jsonmay set it lower and is refused where it sets it higher (decision 5); the guard chapter'squestionrow 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'squestionsection 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.jsonsetsquestion.rowsto 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, thelimitsthe 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
Defaultwith the three fields replaced; the hook and the verb pass it toCheckLimits. Approaches 1, 2, and 4. Tests:TestQuestionLimitsReplaceThreeFieldsOnlyininternal/core/layered/question_test.go(a fixture with all three keys; every other field equalsDefault's);TestGuardQuestionChecksWithTheRepositorysLimitsininternal/surface/cli/guard_question_verb_test.go(a 30-row tab under rows 40 has no rows finding; 41 has one naming 40);TestHookAdmitsThirtyRowsUnderFortyAndNotesFortyOneinguard_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:
TestQuestionLimitsLetThePersonTightenininternal/core/layered/question_test.go, three fixtures: Rows 24 in the machine file under a repository that set 40 (24 in force,limits_source.rowsthe 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 andbundled); each assertsKeyRefusedError'sLimitandLimitOrigin, 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:
TestDefaultRowsIsThirtyTwoAboveTheFloorininternal/core/question/limits_test.go(Default.Rowsis 32,Boundsforquestion.rowsis 24 to 60, andAskingRules(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, assertingKeyRefusedError's fields (a personal value below the floor is refused naming the bounds, not the limit) and thatBoundsis the text's source (TestBoundsAreStatedOnceininternal/core/question/limits_test.gogreps 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:
TestHookDenyNamesTheRefusedKeyAndNoRuleandTestUnlabelledQuestionRunsUncheckedUnderARefusedValueinguard_question_limits_test.go; a refused-value fixture is added totestdata/questions/corpus/, soTestGuardQuestionAndHookDecideAlikeholds the verb and the hook to one answer. - The verb exits 2 with
refusedand noready. 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 rulesprints it, every other domain loads. Approaches 7 to 9. Tests:TestLoadWithARefusalWritesItIntoGrillAndTheNotesininternal/core/rules/grill_test.go;TestRulesPrintsTheRefusedLimitOnStderrininternal/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:
TestAskingRulesFromNamesTheFileThatSetTheLimitsandTestAskingRulesWithNoNoteAreUnchanged(byte equality withAskingRules(Default)) ininternal/core/question/asking_test.go. sourcestaysbundledand the heading carries no label. Approach 8. Test:TestConfiguredRepositoryKeepsGrillBundledinrules_grill_domain_test.go, besideTestAbcdsOwnRulesReportGrillBundled.limits,limits_source,rows_limit, and the injected figures agree from one fixture. Approaches 5, 8, and 9 (the Mechanism claim). Test:TestCheckAndGrillStateOneLimitsininternal/surface/cli/guard_question_verb_test.gorenders 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 inlimitsare the words the layout rule states,rows_limitequalslimits.rows, and every entry oflimits_sourceis.abcd/config.json; a second fixture adds a machine file setting rows 30 and asserts both render 30,limits_source.rowsis the machine file, and the other two entries stay the repository's.- A changed value re-injects the domain. Approach 8. Test:
TestChangedLimitsReinjectGrillininternal/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
limitsfield; the planning page carries the same sentence. Approach 10. Tests:TestDrafterBlockPointsAtTheChecksLimitsandTestIntentPageBlockCarriesTheSameLimitsSentenceincmd/asking-sync/main_test.go;TestDrafterBlockIsGeneratedandTestIntentPageAskingBlockIsGeneratedhold the committed pages. - Three lab runs at rows 40 return one part, report
limits, and read no file. Approach 10. The lab'sINTENTION.mdstates the pass rules before any run; each transcript is read for the verb calls, the returnedlimits, 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:
TestWriterPageStatesItsRefusalsAndFallbacksininternal/core/lintextended to the two sentences. - Every floor and ceiling combination admits the minimal question and refuses the four broken forms. Approach 1. Test:
TestEveryBoundCombinationKeepsTheFixedRulesininternal/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), andTestWrittenSampleQuestionPassesTheLimitsAtTheFloors(new,internal/core/interview/written_test.go). Each measures withquestion.Floors(question.Default). - An unreadable
.abcd/config.jsonis loud and leaves the question checked at the defaults; the verb exits 2 naming it. Approaches 3 and 4. Tests:TestHookChecksAtTheDefaultsOnASymlinkedConfigandTestHookChecksAtTheDefaultsOnAnUnparseableConfiginguard_question_limits_test.go(a well-built question runs at exit 1 with theNOT CHECKEDline naming the file, one that breaks a rule is refused on it, and a rows note names the default),TestHookChecksAtTheDefaultsWithHomeUnsetfor an unresolved home, andTestGuardQuestionExitsTwoNamingAnUnreadConfig(partialtrue,unreadnaming the file, findings at the defaults). - An unreadable
~/.abcd.noindex/config.jsonwith noquestionkey fails open and names the file; a symlinked~/.abcd.noindexwith no file is silent. Approach 3. Test:TestQuestionLimitsAndTheSymlinkedHomeininternal/core/layered/question_test.go, laid out both ways afterhome_link_test.go's pattern, andTestHookNamesAnUnreadMachineConfiginguard_question_limits_test.go. - A five-tab call is refused on the tabs rule at every ceiling. Approach 1. Test:
TestTabsStayFixedAtTheCeilingsinlimits_test.go. - Only the resolver, asking-sync, the bundled layer, and tests pass
Default. Approach 6. Test:TestOnlyTheResolverPassesTheDefaultLimitsininternal/surface/cli/limits_callsites_test.go, a tree walk over non-test Go files with the allowlist by path, failing on any otherquestion.Defaultpassed to the named functions or any read of its three configurable fields. - The bounds are stated once. Approach 1.
TestBoundsAreStatedOnceabove. - Brief, page, and gates. Approach 10. Tests:
TestConfigurationChapterListsTheQuestionKeysininternal/core/lint(the three keys, defaults, and bounds underquestion); thesurface_coveragelint over the guard chapter's table (thequestionrow's bucket and status unchanged, the exit 2 named in prose);TestGuardPageDocumentsTheQuestionVerbextended to the limits sentence;docs-lintand 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 isTestGuardQuestionExitsTwoOnARefusedValuewithTestHookDenyNamesTheRefusedKeyAndNoRuleand the install'sTestInstallRefusesBeforeItsFirstQuestionOnARefusedValueininternal/core/ahoy; the fourth is the verb's text render testTestGuardQuestionTextStatesTheLimitsInForce; the fifth isTestAskingRulesWithNoNoteAreUnchangedwith 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 isTestHookJudgesAtThePersonsTighterRowsinguard_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
- 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
- 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
- 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.
- 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
| id | spc-2610301950037469 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610301950037469-a-repository-sets-the-layout-limits-of.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610301914350541A repository sets the layout limits of abcd's questionsimplements
mentions5
- dcn-26110119440396012026-11-01 — Land PR #1333 (step 2 of spc-2610301950037469) with iss-2611011943547454 open rather than fixing it first. The finding is one sentence in abcd guard question's text render; the JSON and the exit code are correct. It shows only when a draft's only findings are non-refusing structure findings while a store cannot be read. A fix now would cost another full preflight and review round, at one to two hours each, on a machine whose load the product thinker has made the first priority. The fix goes to a drain lane: the issue is a nitpick inconsistency with found_at internal/surface/cli/guard_question_verb.go, which the drain can reserve and fix.
- iss-2611011331010643A build lane can start a spec step whose wait on another spec's step is written only in prose, before that step has landed. A step's needs: line takes only the same spec's step numbers (internal/core/spec/steps.go), so a dependency across specs goes in prose. spc-2610301950037469 step 2 says it waits for spc-2610301917245442's steps 1 and 3; the readiness check cannot read that. On 2026-11-01 lane 2 of run-2610312053537139 started step 2 while the parent's step 1 was still on an unmerged pull request (#1315) and its step 3 had not been built. The implementer built the hook half, could not build the verb half, and the review returned FIX FIRST. Landing would have marked the whole step landed, and step 3, which reads the verb's limits field, would have started next. A full implement and preflight cycle was spent on a step that could not be finished.
- iss-2611011943547454abcd guard question's text render can contradict its own findings list. Take any draft whose only findings are non-refusing structure findings, for example an --ask draft with a valid chip but no id, or a duplicate id or option value. The input composed for the hook is clean. If the mode store or the away record cannot be read as well, the text prints 'this question was checked and has no finding, but …' and, a few lines later, 'findings: 1, 0 of them refusing'. The JSON and the exit code (2) are correct, so this is the text render's wording alone. The line comes from step 2 of spc-2610301950037469 (PR #1333), in internal/surface/cli/guard_question_verb.go. It was found by that lane's delta review on 2026-11-01.
- itd-2610230756259215Every question abcd asks passes its check before the person sees it
- spc-2610301917245442Every question abcd asks passes its check before the person sees it