@debonet/idd 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INSTRUCTIONS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # IDD instructions for coding agents
2
2
 
3
+ <!-- @implements g-mfqz8n@63b06e1 -->
4
+
3
5
  This project uses IDD when its `AGENTS.md` says so. The npm package supplies
4
6
  the agent workflow and commands. The hosted questionnaire runs separately at
5
7
  `https://interrogation.dev`; installing the npm package does not install or
@@ -33,6 +35,13 @@ and grant it access to that repository.
33
35
  thoughts in creation order. Work on a note when it applies and has not
34
36
  already been addressed. Ask through the usual IDD question flow when a
35
37
  note needs a decision. Leave submitted notes in the shared log.
38
+ 6. Before incorporating an answer or acting on a thought or conflict, claim
39
+ that item with `npm exec -- idd claim <answer|thought|conflict>
40
+ <item-id> <session-id>`. Commit and push `idd/claims.json` before starting
41
+ the work. If the push is rejected, sync and inspect the branch; proceed
42
+ only if this session owns the claim. Release it with
43
+ `npm exec -- idd release <kind> <item-id> <session-id>` after the work is
44
+ incorporated, then commit and push the release.
36
45
 
37
46
  For a project's first IDD branch, get its numeric GitHub repository id and
38
47
  run `npm exec -- idd branch-hash <repository-id>
@@ -86,6 +95,15 @@ with `sChoice`, `sNote` and `dtm` (the current ISO 8601 time). Commit and
86
95
  push the answer files to the IDD branch before continuing. Commit and push
87
96
  changes to `pending.json` when you record an answer and after you clear it,
88
97
  so another session can see the remaining work.
98
+
99
+ In Codex, connect the IDD MCP server once with
100
+ `codex mcp add idd --url https://interrogation.dev/mcp`, then restart Codex.
101
+ While waiting for a hosted answer, keep an `idd_wait_for_change` MCP call active
102
+ and run `npm exec -- idd wait <question-id>` as a second listener. The MCP tool
103
+ signals any IDD branch change; `idd wait` synchronizes and records that
104
+ question's answer. After either signal, inspect the latest branch state. Both
105
+ may report the same update, so check pending data before incorporating an answer.
106
+
89
107
  Never replace an answer already accepted on the IDD branch; show the
90
108
  accepted answer if another submission conflicts.
91
109
 
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@debonet/idd",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Interrogation-driven development: the questions page and its command",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/debonet/idd"
8
+ },
5
9
  "publishConfig": {
6
10
  "access": "public"
7
11
  },
@@ -21,15 +25,18 @@
21
25
  "specs/",
22
26
  "templates/"
23
27
  ],
24
- "workspaces": [ "web" ],
28
+ "workspaces": [
29
+ "web"
30
+ ],
25
31
  "bin": {
26
32
  "idd": "bin/idd",
27
33
  "idd-merge-hook": "bin/idd-merge-hook"
28
34
  },
29
35
  "scripts": {
30
36
  "start": "node web/server.js",
31
- "deploy": "node scripts/deploy.js",
32
- "test": "jest",
37
+ "build:web": "node scripts/build-web.js",
38
+ "deploy": "npm run build:web && node scripts/deploy.js",
39
+ "test": "npm run build:web && jest",
33
40
  "spec-map": "$HOME/@concord/bin/spec-map",
34
41
  "spec-coverage": "$HOME/@concord/bin/spec-coverage",
35
42
  "hook-install": "$HOME/@concord/bin/concord-hook install"
@@ -38,12 +45,15 @@
38
45
  "node": ">=20"
39
46
  },
40
47
  "devDependencies": {
48
+ "esbuild": "^0.28.2",
41
49
  "jest": "^29.7.0",
42
50
  "jsdom": "^24.1.0"
43
51
  },
44
52
  "jest": {
45
53
  "rootDir": ".",
46
- "roots": [ "<rootDir>/test" ],
54
+ "roots": [
55
+ "<rootDir>/test"
56
+ ],
47
57
  "testEnvironment": "node"
48
58
  }
49
59
  }
package/scripts/check.js CHANGED
@@ -26,6 +26,9 @@
26
26
  // @implements s-r40lfj@d88f3b2
27
27
  // @implements s-g5gxep@784430f
28
28
  // @implements s-61lmfb@6451bc6
29
+ // @implements s-0u0b06@9b94e8d
30
+ // @implements s-8ffele@5deea72
31
+ // @implements s-upxu0m@00fd9f6
29
32
  const crypto = require( "crypto" )
30
33
  const sFilePage = "idd/page.json"
31
34
  const sFileQuestion = "idd/questions.json"
@@ -35,6 +38,7 @@ const sFileStatus = "idd/status.json"
35
38
  const sFileHistory = "idd/history.json"
36
39
  const sFileConflict = "idd/conflicts.json"
37
40
  const sFileThoughts = "idd/thoughts.json"
41
+ const sFileClaims = "idd/claims.json"
38
42
 
39
43
  function fbString( x ){
40
44
  return typeof x == "string" && x.length > 0
@@ -388,6 +392,28 @@ function fvsStatusProblem( aStatus ){
388
392
  })
389
393
  }
390
394
 
395
+ function fvsClaimsProblem( aClaims, aWorkIds ){
396
+ if( !fbObject( aClaims )) return [ "not a JSON object" ]
397
+ return Object.entries( aClaims ).flatMap(([ sKey, aClaim ]) => {
398
+ const vsProblem = []
399
+ const aMatch = sKey.match( /^(answer|thought|conflict):([0-9a-f]{64})$/ )
400
+ if( !aMatch ) vsProblem.push( sKey + ": invalid claim key" )
401
+ if( !fbObject( aClaim )) return [ ...vsProblem, sKey + ": claim is not an object" ]
402
+ vsProblem.push( ...fvsMemberProblem( aClaim,
403
+ [ "sessionId", "claimedAt" ], [ "sessionId", "claimedAt" ], sKey + ": " ))
404
+ if( "sessionId" in aClaim && !/^[A-Za-z0-9._:-]{1,128}$/.test( aClaim.sessionId )){
405
+ vsProblem.push( sKey + ": invalid session id" )
406
+ }
407
+ if( "claimedAt" in aClaim && !fbIsoTime( aClaim.claimedAt )){
408
+ vsProblem.push( sKey + ": claimedAt is not an ISO 8601 time" )
409
+ }
410
+ if( aMatch && aWorkIds[ aMatch[ 1 ]] && !aWorkIds[ aMatch[ 1 ]].has( aMatch[ 2 ])){
411
+ vsProblem.push( sKey + ": item id is not present in its source file" )
412
+ }
413
+ return vsProblem
414
+ })
415
+ }
416
+
391
417
  function faActiveQuestionIds( sTextQuestion ){
392
418
  try {
393
419
  const vaQuestion = JSON.parse( sTextQuestion )
@@ -493,7 +519,7 @@ function fvsParsed( sFile, sText, fvsProblem ){
493
519
  function fvsProblem(
494
520
  sTextPage, sTextQuestion, sTextAnswer = null, sTextPending = null,
495
521
  sTextStatus = null, sTextHistory = null, sTextConflict = null,
496
- sTextThoughts = null
522
+ sTextThoughts = null, sTextClaims = null
497
523
  ){
498
524
  const aQuestionOfId = faQuestionOfId( sTextQuestion, sTextHistory )
499
525
  const vsActiveQuestionId = faActiveQuestionIds( sTextQuestion )
@@ -527,6 +553,34 @@ function fvsProblem(
527
553
  ...( sTextThoughts === null ? [] : fvsParsed(
528
554
  sFileThoughts, sTextThoughts, fvsThoughtsProblem
529
555
  )),
556
+ ...( sTextClaims === null ? [] : fvsParsed(
557
+ sFileClaims, sTextClaims, ( aClaims ) => {
558
+ const faIds = ( sText, sName ) => {
559
+ try {
560
+ const aData = JSON.parse( sText || ( sName === "thought" ? "[]" : "{}" ))
561
+ return new Set( sName === "conflict"
562
+ ? Array.isArray( aData ) ? aData.map(( a ) => a && a.id ) : []
563
+ : sName === "thought"
564
+ ? Array.isArray( aData ) ? aData.map(( a ) => a && a.id ) : []
565
+ : Object.keys( fbObject( aData ) ? aData : {} ))
566
+ } catch( xError ){ return new Set() }
567
+ }
568
+ return fvsClaimsProblem( aClaims, {
569
+ answer: new Set([
570
+ ...( () => {
571
+ try {
572
+ const vaQuestion = JSON.parse( sTextQuestion || "[]" )
573
+ return Array.isArray( vaQuestion )
574
+ ? vaQuestion.map(( a ) => a && a.id ) : []
575
+ } catch( xError ){ return [] }
576
+ })(),
577
+ ...Object.keys( sTextAnswer === null ? {} : faAnswerOrEmpty( sTextAnswer )),
578
+ ]),
579
+ thought: faIds( sTextThoughts, "thought" ),
580
+ conflict: faIds( sTextConflict, "conflict" ),
581
+ })
582
+ }
583
+ )),
530
584
  ]
531
585
  }
532
586
 
@@ -535,4 +589,5 @@ module.exports = {
535
589
  sFilePending, fsQuestionId, fsLegacyQuestionId, fsBranchHash, faQuestionOptions, sXOptionText,
536
590
  sFileStatus, sFileHistory, sFileConflict, fvsStatusProblem, faQuestionOfId,
537
591
  sFileThoughts, fvsThoughtsProblem, fsThoughtId,
592
+ sFileClaims, fvsClaimsProblem,
538
593
  }
@@ -5,6 +5,7 @@
5
5
  // @implements s-s1eg2z@e4283c0
6
6
  // @implements s-hsle36@555b1d8
7
7
  // @implements s-7wfrt9@cf88f5c
8
+ // @implements s-q3kge1@ba97ef3
8
9
  // @implements s-854kw7@d393719
9
10
  // @implements s-y1y75p@326b683
10
11
  // @implements s-zrui8r@5a4422e
@@ -61,6 +62,10 @@
61
62
  // @implements s-14wrxl@7a4474b
62
63
  // @implements s-amd1ad@5c9c949
63
64
  // @implements s-8hvj7z@4ee0664
65
+ // @implements s-2jdc1d@964ddde
66
+ // @implements s-ky6iv4@0879c7a
67
+ // @implements s-7pdsig@761907d
68
+ // @implements s-7uz5pq@ea5a63d
64
69
  // @implements s-wapj39@5412047
65
70
  // @implements s-twjecx@96950c4
66
71
  // @implements s-g5gxep@784430f
@@ -73,7 +78,7 @@ const {
73
78
  fvsProblem, fvsAddProblem, fbIsoTime, sFilePage, sFileQuestion, sFileAnswer,
74
79
  sFilePending, fsQuestionId, fsLegacyQuestionId, fsBranchHash, faQuestionOptions,
75
80
  sFileStatus, fvsStatusProblem,
76
- sFileHistory, sFileConflict, faQuestionOfId, sFileThoughts,
81
+ sFileHistory, sFileConflict, faQuestionOfId, sFileThoughts, sFileClaims,
77
82
  } = require( "./check" )
78
83
  const { fxWithLock } = require( "./project-lock" )
79
84
  const { fsHtml } = require( "./page-build" )
@@ -93,6 +98,8 @@ const sUsage = [
93
98
  " idd import <page>",
94
99
  " idd answer <id> (a JSON answer on stdin)",
95
100
  " idd status <session-id> <blocked|working|clear>",
101
+ " idd claim <answer|thought|conflict> <item-id> <session-id>",
102
+ " idd release <answer|thought|conflict> <item-id> <session-id>",
96
103
  " idd count [--nosync] [--type <c|u|a|s|conflicts|unanswered|answered|superseded>]",
97
104
  " idd thoughts [--nosync]",
98
105
  " idd clear-pending <id> <time>",
@@ -164,7 +171,7 @@ function aViewerUrl( project, vsArg ){
164
171
  try {
165
172
  uViewer = new URL(
166
173
  process.env.IDD_VIEWER_URL ||
167
- "https://idd-service-778162731919.us-east4.run.app"
174
+ "https://interrogation.dev"
168
175
  )
169
176
  } catch( xError ){
170
177
  return aResult( 2, "", "IDD_VIEWER_URL is not a valid URL\n" )
@@ -195,7 +202,7 @@ async function fOpenEventStream(){
195
202
  try {
196
203
  const uViewer = new URL(
197
204
  process.env.IDD_VIEWER_URL ||
198
- "https://idd-service-778162731919.us-east4.run.app"
205
+ "https://interrogation.dev"
199
206
  )
200
207
  if( ![ "http:", "https:" ].includes( uViewer.protocol )) return null
201
208
  uEvents = new URL( "/events", uViewer )
@@ -408,7 +415,7 @@ function fvsProblemOf( project ){
408
415
  project.sReadOrNull( sFileAnswer ), project.sReadOrNull( sFilePending ),
409
416
  project.sReadOrNull( sFileStatus ), project.sReadOrNull( sFileHistory ),
410
417
  project.sReadOrNull( sFileConflict ),
411
- project.sReadOrNull( sFileThoughts )
418
+ project.sReadOrNull( sFileThoughts ), project.sReadOrNull( sFileClaims )
412
419
  )
413
420
  }
414
421
 
@@ -645,6 +652,58 @@ function aStatus( project, vsArg ){
645
652
  return aResult( 0 )
646
653
  }
647
654
 
655
+ function aWorkClaim( project, vsArg, bRelease ){
656
+ const [ sKind, sItemId, sSessionId ] = vsArg
657
+ if( ![ "answer", "thought", "conflict" ].includes( sKind ) ||
658
+ !/^[0-9a-f]{64}$/.test( sItemId ) ||
659
+ !/^[A-Za-z0-9._:-]{1,128}$/.test( sSessionId )){
660
+ return aResult( 2, "", "claim kind, item id or session id is invalid\n" )
661
+ }
662
+ const vsProblem = fvsProblemOf( project )
663
+ if( vsProblem.length ) return aProblem( vsProblem )
664
+ const sTextClaims = project.sReadOrNull( sFileClaims )
665
+ const aClaims = sTextClaims === null ? {} : aParsedOrNull( sTextClaims )
666
+ if( !fbObject( aClaims )) return aResult( 1, "", sFileClaims + ": not a JSON object\n" )
667
+ const sKey = sKind + ":" + sItemId
668
+ const aExisting = aClaims[ sKey ]
669
+ if( bRelease ){
670
+ if( !aExisting ) return aResult( 0 )
671
+ if( aExisting.sessionId !== sSessionId ){
672
+ return aResult( 1, "", "work item is claimed by " + aExisting.sessionId + "\n" )
673
+ }
674
+ delete aClaims[ sKey ]
675
+ } else {
676
+ if( aExisting ){
677
+ if( aExisting.sessionId === sSessionId ) return aResult( 0 )
678
+ return aResult( 1, "", "work item is claimed by " + aExisting.sessionId + "\n" )
679
+ }
680
+ const aAvailableIds = sKind === "answer"
681
+ ? faQuestionOfId( project.sReadOrNull( sFileQuestion ), project.sReadOrNull( sFileHistory ))
682
+ : aArrayIds( project.sReadOrNull( sKind === "thought" ? sFileThoughts : sFileConflict ))
683
+ if( !( sItemId in aAvailableIds ) && !aAvailableIds.has?.( sItemId )){
684
+ return aResult( 2, "", "item id is not present in its source file\n" )
685
+ }
686
+ aClaims[ sKey ] = { sessionId: sSessionId, claimedAt: new Date().toISOString() }
687
+ }
688
+ project.writeJson( sFileClaims, aClaims )
689
+ return aResult( 0 )
690
+ }
691
+
692
+ function aArrayIds( sText ){
693
+ try {
694
+ const vaData = JSON.parse( sText || "[]" )
695
+ return new Set( Array.isArray( vaData ) ? vaData.map(( a ) => a && a.id ) : [] )
696
+ } catch( xError ){ return new Set() }
697
+ }
698
+
699
+ function aClaim( project, vsArg ){
700
+ return aWorkClaim( project, vsArg, false )
701
+ }
702
+
703
+ function aRelease( project, vsArg ){
704
+ return aWorkClaim( project, vsArg, true )
705
+ }
706
+
648
707
  function aCount( project, vsArg ){
649
708
  let bNoSync = false
650
709
  let sType = null
@@ -802,6 +861,8 @@ const aSpec = {
802
861
  "answer": { cArg: 1, fa: aAnswer, bNeedsPage: true, bWrites: true },
803
862
  "clear-pending": { cArg: 2, fa: aClearPending, bNeedsPage: true, bWrites: true },
804
863
  "status": { cArg: 2, fa: aStatus, bNeedsPage: true, bWrites: true },
864
+ "claim": { cArg: 3, fa: aClaim, bNeedsPage: true, bWrites: true },
865
+ "release": { cArg: 3, fa: aRelease, bNeedsPage: true, bWrites: true },
805
866
  }
806
867
 
807
868
  function aRun( sRoot, vsArg, fsStdin = () => "" ){
package/specs/build.md CHANGED
@@ -11,10 +11,14 @@ invokes; no fixer runs `node`, `jest` or an ad-hoc pipeline outside these.
11
11
 
12
12
  * {#g-94bsyg <arch for=main,architect,fixer via=prompt} Use Node >= 20 and plain
13
13
  JavaScript with CommonJS modules (`require` / `module.exports`) for the
14
- command and its modules; do not add TypeScript, ESM or a bundler.
14
+ command and server. The browser client uses Preact and is bundled into
15
+ same-origin static assets.
15
16
  * {#g-d4bbao <arch for=main,architect,fixer via=prompt} Keep Jest at major version
16
17
  29, and test the page in a DOM emulated by `jsdom`, with the page's store
17
18
  and comment services replaced by fakes the test controls.
19
+ * {#s-0bt8j8 <arch} The browser client is rendered with Preact and emitted as a
20
+ same-origin static asset by `npm run build:web`; `npm test` and
21
+ `npm run deploy` build that asset before use.
18
22
  * {#g-kv15zk <arch for=main,architect,fixer via=prompt} Keep this an
19
23
  interpreted-language project. Developers install its dependencies with
20
24
  `npm install`; adopters add the public package to their project with
@@ -67,6 +71,10 @@ invokes; no fixer runs `node`, `jest` or an ad-hoc pipeline outside these.
67
71
  `@google-cloud/firestore` to retain MCP event subscriptions across Cloud Run
68
72
  restarts; production supplies a Firestore database and grants the Cloud Run
69
73
  service account Firestore read/write access.
74
+ * {#g-vk03sm <arch for=main,architect,fixer via=prompt} The hosted service
75
+ uses Firestore for per-GitHub-user theme preferences as well as MCP event
76
+ subscriptions; user theme records use a separate collection and do not store
77
+ access tokens or answer data.
70
78
  * {#g-u68buh <arch for=main,architect,fixer via=prompt} Keep
71
79
  `@google-cloud/firestore` and `standardwebhooks` in the hosted workspace's
72
80
  runtime dependencies and
@@ -118,6 +126,23 @@ invokes; no fixer runs `node`, `jest` or an ad-hoc pipeline outside these.
118
126
  web application, its runtime dependencies, Dockerfile and Cloud Run deployment
119
127
  script. The hosted service has a separate workspace manifest and is deployed
120
128
  from the IDD repository; it is reached at `https://interrogation.dev`.
129
+ > {#m-r4balx} RULING (CEO), 2026-11-12: “yes, let's do that.” This approves
130
+ > GitHub Actions trusted publishing so package releases do not require the
131
+ > maintainer to supply a one-time code for each release.
132
+ * {#s-jm0nq3 <m-r4balx@e53a626} The root package metadata names `https://github.com/debonet/idd`
133
+ as its repository, matching the source repository used by npm trusted
134
+ publishing.
135
+ * {#s-k9c49i <m-r4balx@e53a626} `.github/workflows/publish.yml` publishes only version tags. It
136
+ checks out the source, installs the supported Node and npm versions, runs the
137
+ full test suite, and publishes `@debonet/idd` with `id-token: write` and
138
+ `contents: read`; it stores no npm token.
139
+ * {#s-r6qcwv <m-r4balx@e53a626} Configure npm's trusted publisher for `debonet/idd` and the
140
+ `publish.yml` workflow, allowing direct package publishing. A release tag's
141
+ version matches `package.json`; the workflow uses npm OIDC and publishes
142
+ provenance without an npm access token.
143
+ * {#i-re3pab <s-r6qcwv@8f29404} **Do:** Run `npm trust list @debonet/idd`.
144
+ **Look for:** GitHub Actions trusted publishing for `debonet/idd` with
145
+ `publish.yml` and direct publishing allowed.
121
146
  * {#g-aiemo2 <arch for=main,architect,fixer via=prompt} Publish the package
122
147
  publicly as `@debonet/idd`, exposing `idd` and `idd-merge-hook` from
123
148
  their matching files under `bin/`, so adopters can install it from npm.
@@ -76,6 +76,14 @@ incorporated. Each `[o-…]` citation names an entry of
76
76
  thing preventing progress; use `working` as soon as work can proceed, and
77
77
  `clear` before ending the session. Commit and push each status change so
78
78
  the viewer can show it.
79
+ * {#g-bd17rs <s-7ppa6w@305659d for=main via=prompt} **Listen for hosted changes.**
80
+ Configure the IDD MCP server once with
81
+ `codex mcp add idd --url https://interrogation.dev/mcp`, then restart Codex.
82
+ While waiting on a hosted answer, keep both the MCP
83
+ `idd_wait_for_change` tool and `npm exec -- idd wait <question-id>` active.
84
+ When either reports a change, sync the matching IDD branch and inspect its
85
+ latest answers, thoughts and conflicts. Treat a second signal for the same
86
+ branch update as a duplicate; incorporate an answer only once.
79
87
 
80
88
  ## Dependencies
81
89
 
@@ -57,6 +57,12 @@ answers are pending. Each `[o-…]` citation names an entry of
57
57
  > C) The session names q201 and asks whether to act on it now." His answer:
58
58
  > A.
59
59
 
60
+ > {#m-kh7vgm} THOUGHT (CEO), 2026-11-13: “We need to make sure that
61
+ > when a question is answered (or observation given or conflict resolved.)
62
+ > that only one active agent picks up on that work. Which means that when
63
+ > agents pull a new chunk of work, they need to write back to the Jason file
64
+ > and check it in indicating that they are doing that work.”
65
+
60
66
  ## Starting a session
61
67
 
62
68
  * {#g-3ncq2t <m-vmsvl8@415d9d6 for=main via=prompt} **Find the matching IDD branch first.** At
@@ -92,6 +98,15 @@ answers are pending. Each `[o-…]` citation names an entry of
92
98
  answer. Work on it when it applies and is not already reflected in the
93
99
  project; if it needs a decision, ask a follow-up through the normal IDD
94
100
  question flow. Do not rewrite or remove submitted thoughts.
101
+ * {#g-mfqz8n <m-kh7vgm@5b68e68 for=main via=prompt} **Claim shared work before acting.**
102
+ Before incorporating a pending answer or acting on a submitted thought or
103
+ answer conflict, claim that item's stable id for this session with
104
+ `npm exec -- idd claim <kind> <item-id> <session-id>`. Commit and push
105
+ `idd/claims.json` to the matching IDD branch before starting the work. If a
106
+ push is rejected, sync the branch and inspect the claim; do not start work
107
+ unless this session owns the claim. After the work is incorporated, release
108
+ the claim with `npm exec -- idd release <kind> <item-id> <session-id>`, then
109
+ commit and push the release.
95
110
 
96
111
  ## Every decision goes through the tool's questionnaire
97
112
 
@@ -87,6 +87,28 @@ projects in one viewer.
87
87
  > access to more repositories. I think I may have only approved the one
88
88
  > project (idd) at this point.”
89
89
 
90
+ > {#m-babliv} BUG REPORT (CEO), 2026-11-13: “OMG. Sometimes the text
91
+ > field gets erased while I'm typing in it. What is happening? Is the page
92
+ > refreshing and wiping out my in-progress work?”
93
+
94
+ > {#m-zqh3c0} BUG REPORT (CEO), 2026-11-13: “Importantly also: the page
95
+ > scroll should not change when being updated. It is quite jarring.”
96
+
97
+ > {#m-6nlxee} BUG REPORT (CEO), 2026-11-13: “Better, but the text I was
98
+ > editing scrolled when the page updated (unsure if my caret moved, but the
99
+ > scroll of the textbox went back to 0 and I could not see the bottom/most
100
+ > recent part of the entry)”
101
+
102
+ > {#m-mwyvc9} RULING (CEO), 2026-11-13: “Yes. Do it. Each item already has a
103
+ > stable id, so use that for the root of the element key”
104
+
105
+ > {#m-sv3g4i} BUG REPORT (CEO), 2026-11-13: “when you sign in using
106
+ > the menu, the menu should go away. Today it remains open.”
107
+
108
+ > {#m-3qt6qa} “I'm still seeing
109
+ > submitted observations in the observations box. They should be moved to the
110
+ > answered section.”
111
+
90
112
  > {#m-h21kou} RULING (CEO), 2026-11-10: “Also, don't show the branch in the
91
113
  > special case where it is \"main\"
92
114
  >
@@ -387,6 +409,19 @@ projects in one viewer.
387
409
  automatically detects changes to IDD branches and refreshes their displayed
388
410
  questionnaire data without requiring a page reload. It preserves unsent
389
411
  answer drafts and the current dashboard or questionnaire view.
412
+ * {#s-o1emm0 <m-babliv@56a9a30} A background questionnaire refresh preserves the
413
+ unsent text in the Thoughts and observations field, as well as unsent answer
414
+ drafts and the current page view.
415
+ * {#s-nedtaj <m-zqh3c0@ebc8c99} When a background data refresh redraws the dashboard or
416
+ questionnaire, the page retains its scroll position.
417
+ * {#s-b7mfmz <m-6nlxee@8de6a06} When a background questionnaire refresh redraws a text
418
+ field, it restores that field's internal scroll position, selection and
419
+ focus, so the person can continue editing where they left off.
420
+ * {#s-0ez7w1 <m-mwyvc9@7e88d5d} A questionnaire item that remains present across a background refresh or
421
+ reorder retains its rendered root and any in-progress editing state; stable
422
+ item ids distinguish questions, thoughts, answers and conflicts.
423
+ * {#s-np9p6t <m-sv3g4i@5b705c0} After GitHub sign-in succeeds, the navigation menu
424
+ closes.
390
425
  * {#s-1v9p2k <m-c37rfq@5dbe195} The GitHub App's push webhook is validated with
391
426
  its configured HMAC secret. Only pushes to refs beginning
392
427
  `refs/heads/idd-` trigger a viewer notification; webhook payloads are not
@@ -415,6 +450,10 @@ projects in one viewer.
415
450
  * {#s-jzlic8 <arch} The `/mcp` endpoint exposes the standard MCP tool
416
451
  `idd_wait_for_change`; an active call waits up to 240 seconds for an IDD
417
452
  branch push and returns only whether a change occurred.
453
+ * {#s-7ppa6w <arch} A Codex session may wait for changes through both the IDD
454
+ MCP `idd_wait_for_change` tool and `idd wait <question-id>` at once. Either
455
+ can signal a change; the session synchronizes and inspects the latest IDD
456
+ branch, treating duplicate signals idempotently.
418
457
  * {#s-udvxd8 <arch} The hosted service stores MCP event subscriptions durably,
419
458
  including their callback URL, signing secret, event name, and expiration,
420
459
  and retains them across Cloud Run restarts.
@@ -472,9 +511,10 @@ projects in one viewer.
472
511
  idea or observation. Submitting non-empty text appends a thought to that
473
512
  project's current IDD branch; the form remains available for another
474
513
  submission.
475
- * {#s-zexwsh <arch} The section displays submitted thoughts with their stable ids,
476
- submission times, and text as plain text. It does not display thought text
477
- as HTML or treat thoughts as questions requiring an answer.
514
+ * {#s-zexwsh <m-yxktyi@b0efeab,m-3qt6qa@4b64c8f} Immediately after a thought is submitted successfully, its card appears in the Answered section with its
515
+ stable id, submission time and text as plain text. The Thoughts and
516
+ observations section holds only the submission form. Thought text is never
517
+ displayed as HTML or treated as a question requiring an answer.
478
518
  * {#s-pla1x4 <arch} Submitting a thought writes directly from the browser to the
479
519
  repository's `idd/thoughts.json` on its IDD branch using GitHub access
480
520
  already granted to the signed-in user. A user without repository write
@@ -524,6 +564,15 @@ projects in one viewer.
524
564
  `https://interrogation.dev`, install `@debonet/idd`, resolve the installed
525
565
  package's `INSTRUCTIONS.md` path with `require.resolve`, and follow those
526
566
  instructions to set up IDD in the current project.
567
+ > {#m-2kt5yx} FEATURE REQUEST (CEO), 2026-11-12: “The installation instructions are not
568
+ > visible on interrogation.dev Are they there? (actually, hiding them in an
569
+ > agent-readable comoment early in the file might be a great idea, e.g.:
570
+ > <!-- if you're an agent that wants to install Interrogation Driven Development
571
+ > (IDD), use npm install @debonet/idd --> )”
572
+ * {#s-iy7nbz <m-2kt5yx@1aed464} The public landing page's HTML starts with a plain HTML comment
573
+ telling agents to install `@debonet/idd`, read its installed
574
+ `INSTRUCTIONS.md`, and use `https://interrogation.dev`; the comment is not
575
+ displayed as page text.
527
576
  * {#s-nq82ty <arch} The viewer signs users in through the IDD
528
577
  GitHub App's user authorization flow and requests only the app's repository
529
578
  contents permission.
@@ -657,6 +706,12 @@ projects in one viewer.
657
706
  * {#s-omsgq8 <arch} `idd/status.json` is an object keyed by active agent session
658
707
  id; each value has exactly a boolean `blocked` field and an ISO 8601
659
708
  `updatedAt` field.
709
+ * {#s-0u0b06 <arch} If `idd/claims.json` exists, it is an object keyed by
710
+ `<kind>:<item-id>` where kind is `answer`, `thought` or `conflict` and the
711
+ item id is a lowercase 64-character hexadecimal id. Each value has exactly
712
+ a valid `sessionId` and an ISO 8601 `claimedAt` field. A claim may name only
713
+ an answer's question id, a submitted thought id, or a conflict id present in
714
+ the corresponding IDD file.
660
715
  * {#s-w9knti <arch} An agent session updates its entry on the
661
716
  matching IDD branch when its blocked state changes, sets `blocked` true
662
717
  only when it cannot continue without a human answer, refreshes `updatedAt`
package/specs/idd-page.md CHANGED
@@ -9,7 +9,8 @@ page file and question file, checks them, builds the questions page from
9
9
  them, adds questions with branch-scoped hash ids, records and clears answers
10
10
  in the project's answer and pending answer files, and imports questions
11
11
  from a page made from the older template; it also reports session status,
12
- prints a hosted viewer link, and counts questions on an IDD branch.
12
+ claims work items between agent sessions, prints a hosted viewer link, and
13
+ counts questions on an IDD branch.
13
14
 
14
15
  ## His words
15
16
 
@@ -43,6 +44,14 @@ prints a hosted viewer link, and counts questions on an IDD branch.
43
44
  encodes both values and uses `IDD_VIEWER_URL` when set, otherwise the
44
45
  package's configured public viewer address. It does not read a GitHub
45
46
  credential or change files.
47
+ > {#m-umjnwb} BUG REPORT (CEO), 2026-11-13: “Somehow the new agent got this for a
48
+ > login page: https://idd-service-778162731919.us-east4.run.app/?repo=AblifyAI%2Fablify&branch=main
49
+ > It should be using interrogation.dev everywhere (that does not work with
50
+ > github sign in!)”
51
+ * {#s-q3kge1 <m-umjnwb@4a707e9} When `IDD_VIEWER_URL` is unset, both `viewer-url` and `wait` use
52
+ `https://interrogation.dev` as their origin. They never use the Cloud Run
53
+ `run.app` address as a viewer link or event endpoint, so GitHub sign-in stays
54
+ on the registered custom domain.
46
55
  * {#s-dal2u7 <arch} **`wait <question-id> [interval-seconds]`** subscribes to
47
56
  the hosted viewer's event stream and checks the matching remote IDD branch
48
57
  through the current worktree's configured Git remote when the wait starts
@@ -84,6 +93,24 @@ prints a hosted viewer link, and counts questions on an IDD branch.
84
93
  every entry by `specs/hosted-questionnaire.md`. Clearing an absent entry
85
94
  succeeds without writing the file.
86
95
 
96
+ ## Work claims
97
+
98
+ * {#s-2jdc1d} **`claim <kind> <item-id> <session-id>`** records that one active
99
+ session owns an answer, thought or conflict work item in `idd/claims.json`.
100
+ It refuses to replace a claim held by another session and reports the
101
+ current owner; repeating a claim by its owner succeeds without changing its
102
+ timestamp.
103
+ * {#s-ky6iv4} **`release <kind> <item-id> <session-id>`** removes a claim only
104
+ when the named session owns it. Releasing an absent claim succeeds; a
105
+ different owner is reported and the file is unchanged.
106
+ * {#s-7pdsig} A claim kind is `answer`, `thought` or `conflict`; an
107
+ item id is a lowercase 64-character hexadecimal id; and a session id follows
108
+ the status command's validation rule. Invalid arguments exit 2. Malformed
109
+ claim data is refused without changing the file and exits 1.
110
+ * {#s-7uz5pq} Claiming authorizes work only after the claim has been
111
+ committed and pushed to the matching IDD branch. A rejected push means the
112
+ session must sync and inspect the remote claim before it starts work.
113
+
87
114
  ## count
88
115
 
89
116
  * {#s-vwase3 <m-xoqfn9@0df3826} **`count`** reports the counts of conflicting, unanswered,
@@ -5,9 +5,9 @@ Statement ids: complete
5
5
  ## Purpose
6
6
 
7
7
  The files on an adopting project's matching IDD branch that hold its shared
8
- questions, answers and pending answers, so sessions of any agent tool work
9
- from the same question list and recorded results, while each tool presents
10
- its own questionnaire.
8
+ questions, answers, pending answers and work claims, so sessions of any agent
9
+ tool work from the same question list and recorded results, while each tool
10
+ presents its own questionnaire.
11
11
 
12
12
  ## His words
13
13
 
@@ -161,6 +161,16 @@ its own questionnaire.
161
161
  * {#s-u52jq6 <arch} **Absent means none pending.** A project with no pending answer file has
162
162
  no locally pending answer; recording the first answer creates it.
163
163
 
164
+ ## Work claims
165
+
166
+ * {#s-8ffele <arch} `idd/claims.json` holds one JSON object keyed by
167
+ `<kind>:<item-id>`. Each value has exactly `sessionId`, the owning agent
168
+ session id, and `claimedAt`, an ISO 8601 time. Kinds are `answer`, `thought`
169
+ and `conflict`; the item id is the stable 64-character id from its source
170
+ file.
171
+ * {#s-upxu0m <arch} If `idd/claims.json` is absent, no work item is claimed;
172
+ the first claim creates it.
173
+
164
174
  ## Thoughts and observations
165
175
 
166
176
  * {#s-61lmfb <arch} `idd/thoughts.json` holds one JSON array of thoughts in append
package/specs/themes.md CHANGED
@@ -31,6 +31,11 @@ per-project theme preference.
31
31
  > or should match the splash page. Either way, the splash page theme should be
32
32
  > a theming option.”
33
33
 
34
+ > {#m-rakcji} THOUGHT (CEO), 2026-11-13: “Theme choice should be shared
35
+ > across browsers, but not across users. Perhaps we should maintain a list of
36
+ > <user>,<theme> in the idd-<branch> directory on github, unless there is
37
+ > another way (e.g. is there github-user storage offered?)”
38
+
34
39
  ## Built-in themes
35
40
 
36
41
  * {#s-ncz9te <m-qqilfo@d782597} The viewer offers the light, dark, retro, silk, and editorial
@@ -64,9 +69,24 @@ per-project theme preference.
64
69
  ## Theme choice and content hooks
65
70
 
66
71
  * {#s-6bc09d <m-qqilfo@d782597} The questionnaire page lets the user choose a theme for
67
- that project and code branch. The viewer saves the choice in that browser
68
- under the repository and branch identity, and keeps that preference when
69
- the user returns to the questionnaire.
72
+ that project and code branch. The viewer saves the choice under the
73
+ authenticated GitHub user, repository and branch identity, and keeps that
74
+ preference when the user returns to the questionnaire in another browser.
75
+ * {#s-jl9nzo <m-rakcji@fd6be8a} A project theme preference is keyed to the
76
+ authenticated GitHub user's immutable account id and the repository and
77
+ branch identity; two users may choose different themes for the same project.
78
+ The splash, dashboard and project theme choices are stored in Firestore and
79
+ shared across browsers for that user. The hosted service does not expose one
80
+ user's preferences to another user.
81
+ * {#s-wmmbpw <m-rakcji@fd6be8a} The browser reads and writes the signed-in user's
82
+ theme choices through `/api/theme-preferences`, authenticating with its
83
+ in-memory GitHub access token. The service verifies the token's GitHub user
84
+ id before reading or writing that user's preference record. Theme preference
85
+ data contains only theme ids and project/repository branch keys;
86
+ it contains no access tokens or answers.
87
+ * {#s-6io4k8 <arch} Before sign-in, the splash page uses a browser-local
88
+ theme choice. After sign-in, the user's saved splash theme replaces that
89
+ browser-local choice.
70
90
  * {#s-eydwk6 <m-fjqj3t@17d6d3e} The splash page and dashboard each have an independent built-in theme
71
91
  selector in the content area directly below the site header; changing either
72
92
  site theme leaves the other surface's theme and each project's questionnaire