@dzhechkov/skills-feature-adr 1.3.43 → 1.3.44

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/README.md CHANGED
@@ -124,6 +124,15 @@ writing code. Without this the run stalls; with `coder: 'codex-fallback'` the pi
124
124
  unavailable (limit?) — falling back to Codex auto"* and finishes the code + tests on Codex, no
125
125
  restart and no lost work.
126
126
 
127
+ **Codex writes are out-of-band — the Step-7.5 landed barrier waits for them.** Codex applies edits via
128
+ its own runtime, so a naive pipeline runs QE before the async write flushes and false-grades *"Step 7
129
+ never ran"* on real code. For a Codex-coded run the pipeline polls a **bounded 120s backing-off window**
130
+ (`1,2,2,5,5,10,10,15,20,25,25`s), preferring the code stage's *declared expected files* when known, and
131
+ emits an explicit `changed=0 after 120s — genuinely not landed` only when the window truly expires — so
132
+ QE distinguishes "not implemented" from "not yet flushed". A Claude-coded run is synchronous and skips
133
+ the barrier entirely (zero added wait). Note: if Codex flushes slower than 120s, re-verify after the run
134
+ rather than trusting the end-of-run grade.
135
+
127
136
  ```bash
128
137
  # Headless login on a VPS (no browser):
129
138
  codex login --device-auth # prints a code + URL you approve on another device
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.43",
3
+ "version": "1.3.44",
4
4
  "description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
5
5
  "bin": {
6
6
  "skills-feature-adr": "./bin/cli.js"
@@ -240,6 +240,116 @@ function stageLabel(baseLabel, opts) {
240
240
  // Codex-QE run, do ZERO extra work (byte-identical to today).
241
241
  function needsLandedBarrier(opts) { return !!(opts && opts.agentType === 'codex:codex-rescue') }
242
242
 
243
+ const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 120000
244
+ const DEFAULT_CODE_LANDING_BACKOFF_MS = [1000, 2000, 2000, 5000, 5000, 10000, 10000, 15000, 20000, 25000, 25000]
245
+ const CODE_LANDED_BARRIER_SLEEPS_SECONDS = DEFAULT_CODE_LANDING_BACKOFF_MS.map((ms) => ms / 1000)
246
+ const CODE_LANDING_PIPELINE_PREFIXES = ['features/', '.dz/', '.agentic-qe/', 'roam/']
247
+
248
+ function codeLandingEmptySignal(seconds) {
249
+ return 'changed=0 after ' + seconds + 's — genuinely not landed'
250
+ }
251
+
252
+ function needsCodeLandedBarrier(coderUsed) {
253
+ return coderUsed === 'codex' || coderUsed === 'codex-fallback'
254
+ }
255
+
256
+ function normalizeCodeLandingPath(path) {
257
+ let p = String(path || '').trim().replace(/\\/g, '/')
258
+ while (p.indexOf('./') === 0) p = p.slice(2)
259
+ p = p.replace(/\/+/g, '/')
260
+ if (!p) return ''
261
+ if (p[0] === '/') return ''
262
+ if (p === '..' || p.indexOf('../') === 0 || p.indexOf('/../') >= 0 || p.endsWith('/..')) return ''
263
+ if (/[\0\r\n\t "'\x60$;&|<>*?()[\]{}!]/.test(p)) return ''
264
+ if (p.endsWith('/')) return ''
265
+ for (let i = 0; i < CODE_LANDING_PIPELINE_PREFIXES.length; i++) {
266
+ const prefix = CODE_LANDING_PIPELINE_PREFIXES[i]
267
+ const bare = prefix.slice(0, -1)
268
+ if (p === bare || p.indexOf(prefix) === 0) return ''
269
+ }
270
+ return p
271
+ }
272
+
273
+ function filterPollableCodePaths(paths) {
274
+ const out = []
275
+ const seen = {}
276
+ for (let i = 0; i < (paths || []).length; i++) {
277
+ const normalized = normalizeCodeLandingPath(paths[i])
278
+ if (!normalized || seen[normalized]) continue
279
+ seen[normalized] = 1
280
+ out.push(normalized)
281
+ }
282
+ return out
283
+ }
284
+
285
+ function codeLandedBarrierPlan(coderUsed, expectedPaths) {
286
+ const enabled = needsCodeLandedBarrier(coderUsed)
287
+ const pollWindowSeconds = DEFAULT_CODE_LANDING_MAX_WAIT_MS / 1000
288
+ if (!enabled) {
289
+ return { enabled: false, mode: 'any-code-change', sleepsMs: [], sleepsSeconds: [], pollWindowMs: 0, pollWindowSeconds: 0, expectedPaths: [], emptySignal: '' }
290
+ }
291
+ const filteredExpectedPaths = filterPollableCodePaths(expectedPaths || [])
292
+ return {
293
+ enabled: true,
294
+ mode: filteredExpectedPaths.length > 0 ? 'expected-files' : 'any-code-change',
295
+ sleepsMs: DEFAULT_CODE_LANDING_BACKOFF_MS,
296
+ sleepsSeconds: CODE_LANDED_BARRIER_SLEEPS_SECONDS,
297
+ pollWindowMs: DEFAULT_CODE_LANDING_MAX_WAIT_MS,
298
+ pollWindowSeconds: pollWindowSeconds,
299
+ expectedPaths: filteredExpectedPaths,
300
+ emptySignal: codeLandingEmptySignal(pollWindowSeconds),
301
+ }
302
+ }
303
+
304
+ function addExpectedCodeTarget(value, out) {
305
+ if (value === null || value === undefined) return
306
+ if (Array.isArray(value)) {
307
+ for (let i = 0; i < value.length; i++) addExpectedCodeTarget(value[i], out)
308
+ return
309
+ }
310
+ if (typeof value === 'object') {
311
+ if (Array.isArray(value.wrote)) addExpectedCodeTarget(value.wrote, out)
312
+ if (Array.isArray(value.paths)) addExpectedCodeTarget(value.paths, out)
313
+ return
314
+ }
315
+ const lines = String(value).split(/\r?\n/)
316
+ for (let i = 0; i < lines.length; i++) {
317
+ const candidate = lines[i].replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim()
318
+ if (candidate) out.push(candidate)
319
+ }
320
+ }
321
+
322
+ function extractExpectedCodeTargetsFromText(text) {
323
+ const out = []
324
+ const lines = String(text || '').split(/\r?\n/)
325
+ let inBlock = false
326
+ for (let i = 0; i < lines.length; i++) {
327
+ const trimmed = lines[i].trim()
328
+ if (/^EXPECTED_CODE_TARGETS:\s*$/i.test(trimmed)) { inBlock = true; continue }
329
+ if (!inBlock) continue
330
+ if (!trimmed) continue
331
+ if (/^[A-Z][A-Z0-9_ -]*:\s*$/.test(trimmed)) break
332
+ out.push(trimmed.replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim())
333
+ }
334
+ return out
335
+ }
336
+
337
+ function extractExpectedCodeTargets(argTargets, codexText) {
338
+ const out = []
339
+ addExpectedCodeTarget(argTargets, out)
340
+ addExpectedCodeTarget(extractExpectedCodeTargetsFromText(codexText), out)
341
+ return filterPollableCodePaths(out)
342
+ }
343
+
344
+ function codeLandingShellQuote(value) {
345
+ return "'" + String(value).replace(/'/g, "'\"'\"'") + "'"
346
+ }
347
+
348
+ function codeLandingProbeCmd(repo, plan) {
349
+ const expectedList = plan.expectedPaths.length > 0 ? plan.expectedPaths.map(codeLandingShellQuote).join(' ') : "''"
350
+ return 'repo=' + codeLandingShellQuote(repo) + '; sleeps="' + plan.sleepsSeconds.join(' ') + '"; expected_count=' + plan.expectedPaths.length + '; elapsed=0; poll(){ paths=$(git -C "$repo" status --porcelain 2>/dev/null | sed -E "s/^...//" | sed -E "s/.* -> //" | grep -vE "^(features/|[.]dz/|[.]agentic-qe/|roam/)" | sed "/^$/d" | head -200); n=$(printf "%s\\n" "$paths" | sed "/^$/d" | wc -l | tr -d " "); if [ "$expected_count" -gt 0 ]; then matched=""; for p in ' + expectedList + '; do [ -z "$p" ] && continue; if printf "%s\\n" "$paths" | grep -Fx -- "$p" >/dev/null; then matched="$p"; break; fi; done; if [ -n "$matched" ]; then echo "CODEX-LANDING-SIGNAL status=landed changed=1 after=${elapsed}s predicate=expected-path"; echo "matched=$matched"; echo "files:"; printf "%s\\n" "$paths" | head -40; exit 0; fi; else if [ "$n" -gt 0 ]; then echo "CODEX-LANDING-SIGNAL status=landed changed=$n after=${elapsed}s predicate=any-code-change"; echo "files:"; printf "%s\\n" "$paths" | head -40; exit 0; fi; fi; }; poll; for wait in $sleeps; do sleep "$wait"; elapsed=$((elapsed + wait)); poll; done; echo "CODEX-LANDING-SIGNAL status=genuinely-not-landed ' + plan.emptySignal + '"; if [ "$expected_count" -gt 0 ]; then echo "predicate=expected-path observed=$n"; else echo "predicate=any-code-change"; fi; echo "files:"; if [ -n "$paths" ]; then printf "%s\\n" "$paths" | head -40; else echo "(none)"; fi'
351
+ }
352
+
243
353
  // A Bash one-liner that waits for a Codex OUT-OF-BAND artifact write to LAND: polls up to ~40s until
244
354
  // the file exists, is non-empty, AND its size is stable across two reads (write finished). Assumes a
245
355
  // fresh feature slug (no stale same-path artifact) — true for a normal /feature-adr run.
@@ -383,6 +493,7 @@ const codeClaudeModel = codeIsCodexFirst ? {} : (codeModel.agentType ? {} : code
383
493
  const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeClaudeModel)
384
494
  let code = null
385
495
  let coderUsed = 'claude'
496
+ let codexCodeText = ''
386
497
  if (!codeIsCodexFirst) {
387
498
  code = await agent(codePrompt, codeClaudeOpts)
388
499
  if (code) { coderUsed = 'claude'; modelsUsed.code = modelLabel(codeClaudeOpts) }
@@ -392,20 +503,22 @@ if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
392
503
  if (CODER === 'codex-fallback' && !codeIsCodexFirst) log('Code: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
393
504
  const codeCodexLabelOpts = codeModel.agentType ? codeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
394
505
  const codeCodexOpts = mergeOpts({ label: stageLabel('code:codex', codeCodexLabelOpts), phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
395
- const codexCode = await agent(codePrompt + CODEX_HINT, codeCodexOpts)
396
- if (codexCode) { code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: String(codexCode).slice(0, 500) }; coderUsed = codeIsCodexFirst ? 'codex' : 'codex-fallback'; modelsUsed.code = modelLabel(codeCodexLabelOpts) }
506
+ const codexExpectedTargetsHint = '\n\nBecause this is running on Codex, include a final EXPECTED_CODE_TARGETS: block listing the repo-relative production/test files you expect to create or modify. List only real code/test/config/docs targets outside features/, .dz/, .agentic-qe/, and roam/. Example:\nEXPECTED_CODE_TARGETS:\n- packages/example/src/file.ts\n- packages/example/test/file.test.ts'
507
+ const codexCode = await agent(codePrompt + CODEX_HINT + codexExpectedTargetsHint, codeCodexOpts)
508
+ if (codexCode) { codexCodeText = String(codexCode); code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: codexCodeText.slice(0, 500) }; coderUsed = codeIsCodexFirst ? 'codex' : 'codex-fallback'; modelsUsed.code = modelLabel(codeCodexLabelOpts) }
397
509
  }
398
510
 
399
511
  // Step 7.5: Codex-landed barrier. Codex applies edits OUT-OF-BAND via its own runtime; without this,
400
- // Step-8 QE reads the tree before the async write flushes and false-grades "Step 7 never ran" (grade D
401
- // on real, landed code observed on the goap-ed25519 crypto fix). Poll git status (excluding pipeline
402
- // artifacts) up to ~30s until real code changes appear, and hand the confirmed file list to QE so it
403
- // reviews the ACTUAL landed changes. Claude-coded runs are synchronous → this barrier is skipped.
512
+ // Step-8 QE can read the tree before the async write flushes and false-grade "Step 7 never ran" on real
513
+ // completed code. Poll a bounded 120s backing-off window, preferring declared expected code targets when
514
+ // known. Claude-coded runs are synchronous this barrier is skipped with zero target parsing/probe work.
404
515
  let landedNote = ''
405
- if (coderUsed === 'codex' || coderUsed === 'codex-fallback') {
406
- const barrierCmd = 'for i in 1 2 3 4 5 6; do n=$(git -C ' + REPO + ' status --porcelain 2>/dev/null | grep -vE "features/|[.]dz/|[.]agentic-qe/|roam/" | wc -l); [ "$n" -gt 0 ] && break; sleep 5; done; echo "changed=$n"; git -C ' + REPO + ' status --porcelain 2>/dev/null | grep -vE "features/|[.]dz/|[.]agentic-qe/|roam/" | head -40'
407
- const probe = await agent('Confirm the Codex Step-7 edits have LANDED in the working tree BEFORE QE runs (Codex writes out-of-band). Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + barrierCmd, { label: 'code:confirm-landed', phase: 'Code' })
408
- landedNote = '\n\nCODEX-CODED (out-of-band): review the CONFIRMED landed working-tree changes below — do NOT report "Step 7 never ran" if files are listed. If changed=0 the implementation genuinely did not land, then grade accordingly.\n' + String(probe || '(landed-probe failed)').slice(0, 1500)
516
+ if (needsCodeLandedBarrier(coderUsed)) {
517
+ const expectedCodeTargets = extractExpectedCodeTargets(A.expectedCodeTargets, codexCodeText)
518
+ const barrierPlan = codeLandedBarrierPlan(coderUsed, expectedCodeTargets)
519
+ const barrierCmd = codeLandingProbeCmd(REPO, barrierPlan)
520
+ const probe = await agent('Confirm the Codex Step-7 edits have LANDED in the working tree BEFORE QE runs (Codex writes out-of-band). Expected-file mode must be satisfied by one of the declared expected paths; unrelated dirty files do not count in that mode. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + barrierCmd, { label: 'code:confirm-landed', phase: 'Code' })
521
+ landedNote = '\n\nCODEX-CODED (out-of-band): Step 7.5 landing barrier used mode=' + barrierPlan.mode + ', window=' + barrierPlan.pollWindowSeconds + 's. Review the signal below. If status=landed, read the listed files and do NOT report "Step 7 never ran". Only status=genuinely-not-landed with "' + barrierPlan.emptySignal + '" means the bounded barrier found no intended code after the full window.\nExpected code targets: ' + (barrierPlan.expectedPaths.length ? barrierPlan.expectedPaths.join(', ') : '(none declared; fallback accepts any non-pipeline code change)') + '\n' + String(probe || '(landed-probe failed)').slice(0, 1500)
409
522
  }
410
523
 
411
524
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach