devflow-kit 3.5.0 → 3.7.0

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.
Files changed (146) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/README.md +10 -6
  3. package/dist/agents/code.md +7 -11
  4. package/dist/agents/{diagnose.md → debug.md} +4 -4
  5. package/dist/agents/{code-fix.md → fix.md} +7 -8
  6. package/dist/agents/{scrutinize.md → harden.md} +6 -6
  7. package/dist/agents/{code-edit.md → refactor.md} +8 -9
  8. package/dist/agents/review.md +2 -0
  9. package/dist/cli/agents-view/render.js +1 -1
  10. package/dist/cli/commands/gates.js +276 -0
  11. package/dist/cli/commands/init-seed.js +9 -1
  12. package/dist/cli/commands/init.js +34 -11
  13. package/dist/cli/commands/learning.js +12 -12
  14. package/dist/cli/commands/legacy-hooks.js +1 -1
  15. package/dist/cli/commands/memory.js +18 -0
  16. package/dist/cli/commands/uninstall.js +111 -5
  17. package/dist/cli.js +2 -0
  18. package/dist/commands/bug-analysis.md +21 -17
  19. package/dist/commands/code-review.md +65 -66
  20. package/dist/commands/debug.md +17 -17
  21. package/dist/commands/dynamic-build.md +104 -116
  22. package/dist/commands/dynamic-plan.md +13 -11
  23. package/dist/commands/dynamic-profile.md +9 -7
  24. package/dist/commands/dynamic-tickets.md +13 -11
  25. package/dist/commands/explore.md +15 -15
  26. package/dist/commands/implement.md +80 -154
  27. package/dist/commands/plan.md +34 -66
  28. package/dist/commands/release.md +11 -7
  29. package/dist/commands/research.md +14 -10
  30. package/dist/commands/resolve.md +44 -88
  31. package/dist/commands/self-review.md +44 -44
  32. package/dist/core/agent-models.js +47 -11
  33. package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
  34. package/dist/core/echo-value.js +42 -0
  35. package/dist/core/evidence-policy.js +17 -5
  36. package/dist/core/gate-level.js +213 -0
  37. package/dist/core/learning-queue-cleanup.js +1 -1
  38. package/dist/core/manifest.js +26 -10
  39. package/dist/core/migrations.js +63 -44
  40. package/dist/core/model-discovery.js +7 -1
  41. package/dist/core/plugins.js +14 -12
  42. package/dist/core/project-paths.js +3 -3
  43. package/dist/core/tracker.js +5 -33
  44. package/dist/hud/codex-usage.js +247 -0
  45. package/dist/hud/components/codex-quota.js +37 -0
  46. package/dist/hud/components/usage-quota.js +1 -1
  47. package/dist/hud/config.js +1 -0
  48. package/dist/hud/index.js +16 -3
  49. package/dist/hud/render.js +3 -1
  50. package/dist/learning-off/agents/code.md +7 -11
  51. package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
  52. package/dist/learning-off/agents/{code-fix.md → fix.md} +7 -8
  53. package/dist/learning-off/agents/{scrutinize.md → harden.md} +6 -6
  54. package/dist/learning-off/agents/{code-edit.md → refactor.md} +8 -9
  55. package/dist/learning-off/agents/review.md +2 -0
  56. package/dist/learning-off/commands/bug-analysis.md +9 -9
  57. package/dist/learning-off/commands/code-review.md +54 -59
  58. package/dist/learning-off/commands/debug.md +6 -10
  59. package/dist/learning-off/commands/dynamic-build.md +98 -112
  60. package/dist/learning-off/commands/dynamic-plan.md +5 -5
  61. package/dist/learning-off/commands/dynamic-profile.md +1 -1
  62. package/dist/learning-off/commands/dynamic-tickets.md +5 -5
  63. package/dist/learning-off/commands/explore.md +4 -8
  64. package/dist/learning-off/commands/implement.md +68 -146
  65. package/dist/learning-off/commands/plan.md +23 -58
  66. package/dist/learning-off/commands/release.md +4 -2
  67. package/dist/learning-off/commands/research.md +2 -2
  68. package/dist/learning-off/commands/resolve.md +33 -81
  69. package/dist/learning-off/commands/self-review.md +32 -36
  70. package/dist/skills/git/references/pr/check-merge-readiness.md +2 -0
  71. package/dist/skills/git/references/pr/update-pr-evidence.md +4 -6
  72. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
  73. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
  74. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
  75. package/dist/targets/claude-code/installer.js +117 -84
  76. package/dist/targets/claude-code/learning-install.js +15 -3
  77. package/dist/targets/claude-code/tracker-servers.js +1 -1
  78. package/package.json +1 -1
  79. package/src/assets/agents/_partials/_code.mds +7 -8
  80. package/src/assets/agents/code.mds +4 -7
  81. package/src/assets/agents/{learning.md → curate.md} +8 -10
  82. package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
  83. package/src/assets/agents/evaluate.md +4 -4
  84. package/src/assets/agents/{code-fix.mds → fix.mds} +5 -5
  85. package/src/assets/agents/{scrutinize.mds → harden.mds} +6 -6
  86. package/src/assets/agents/{simplify.md → polish.md} +4 -4
  87. package/src/assets/agents/{code-edit.mds → refactor.mds} +6 -6
  88. package/src/assets/agents/review.mds +2 -0
  89. package/src/assets/agents/synthesize.md +2 -2
  90. package/src/assets/agents/test.md +2 -2
  91. package/src/assets/commands/_partials/_compliance.mds +1 -1
  92. package/src/assets/commands/_partials/_decisions.mds +26 -6
  93. package/src/assets/commands/_partials/_engine.mds +15 -15
  94. package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
  95. package/src/assets/commands/_partials/_knowledge.mds +14 -9
  96. package/src/assets/commands/_partials/_preamble.mds +4 -2
  97. package/src/assets/commands/_partials/_roster.mds +4 -4
  98. package/src/assets/commands/_partials/_settings.mds +2 -2
  99. package/src/assets/commands/_partials/_wave.mds +10 -17
  100. package/src/assets/commands/bug-analysis.mds +7 -7
  101. package/src/assets/commands/code-review.mds +51 -56
  102. package/src/assets/commands/debug.mds +2 -2
  103. package/src/assets/commands/dynamic-build.mds +67 -74
  104. package/src/assets/commands/implement.mds +65 -127
  105. package/src/assets/commands/plan.mds +21 -61
  106. package/src/assets/commands/release.mds +5 -1
  107. package/src/assets/commands/resolve.mds +28 -76
  108. package/src/assets/commands/self-review.mds +27 -27
  109. package/src/assets/mds/git/_ops_steps.mds +3 -3
  110. package/src/assets/mds/git/_pr.mds +6 -6
  111. package/src/assets/mds/tracker/_common.mds +23 -12
  112. package/src/assets/mds/tracker/_github.mds +2 -9
  113. package/src/assets/mds/tracker/_jira.mds +2 -15
  114. package/src/assets/mds/tracker/_linear.mds +2 -15
  115. package/src/assets/scripts/ci-wait.cjs +22 -4
  116. package/src/assets/scripts/claude-md-audit.cjs +66 -51
  117. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
  118. package/src/assets/scripts/hooks/background-memory-update +404 -95
  119. package/src/assets/scripts/hooks/capture-prompt +1 -1
  120. package/src/assets/scripts/hooks/capture-question +1 -1
  121. package/src/assets/scripts/hooks/capture-turn +1 -1
  122. package/src/assets/scripts/hooks/json-helper.cjs +13 -2
  123. package/src/assets/scripts/hooks/json-parse +18 -3
  124. package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
  125. package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -4
  126. package/src/assets/scripts/hooks/memory-permission +358 -0
  127. package/src/assets/scripts/hooks/preamble +1 -1
  128. package/src/assets/scripts/hooks/session-start-context +60 -47
  129. package/src/assets/scripts/hooks/session-start-memory +26 -5
  130. package/src/assets/scripts/lib/project-config.cjs +19 -4
  131. package/src/assets/scripts/resolve-settings.cjs +200 -32
  132. package/src/assets/scripts/verify-evidence.cjs +726 -9
  133. package/src/assets/skills/agent-references/SKILL.md +14 -6
  134. package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
  135. package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
  136. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
  137. package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
  138. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +1 -1
  139. package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
  140. package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
  141. package/src/assets/skills/apply-decisions/SKILL.md +6 -4
  142. package/src/assets/skills/docs-framework/SKILL.md +3 -3
  143. package/src/assets/skills/qa/SKILL.md +1 -1
  144. package/src/assets/skills/quality-gates/SKILL.md +7 -7
  145. package/src/assets/skills/quality-gates/references/report-template.md +7 -7
  146. /package/src/assets/skills/agent-references/references/{learning → curate}/maintain.md +0 -0
@@ -43,7 +43,7 @@ df_resolve_roots "$CWD" 2>/dev/null || true
43
43
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
44
44
  # The learning queue is the repository's, not the checkout's: in a linked worktree
45
45
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
46
- # the one the Learning agent drains into the shared ledger. Memory stays per checkout.
46
+ # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
47
47
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
48
48
 
49
49
  # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
@@ -62,7 +62,7 @@ df_resolve_roots "$CWD" 2>/dev/null || true
62
62
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
63
63
  # The learning queue is the repository's, not the checkout's: in a linked worktree
64
64
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
65
- # the one the Learning agent drains into the shared ledger. Memory stays per checkout.
65
+ # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
66
66
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
67
67
 
68
68
  # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
@@ -47,7 +47,7 @@ df_resolve_roots "$CWD" 2>/dev/null || true
47
47
  PROJECT_ROOT="${DF_ROOT:-$CWD}"
48
48
  # The learning queue is the repository's, not the checkout's: in a linked worktree
49
49
  # it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
50
- # the one the Learning agent drains into the shared ledger. Memory stays per checkout.
50
+ # the one the Curate agent drains into the shared ledger. Memory stays per checkout.
51
51
  LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
52
52
 
53
53
  # The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/assets/scripts/hooks/json-helper.cjs
4
4
  // Provides jq-equivalent operations for hooks when jq is not installed, and the
5
- // learning ops the Learning agent runs from the project root.
5
+ // learning ops the Curate agent runs from the project root.
6
6
  // SECURITY: This is a local CLI helper invoked only by shell hooks and the
7
- // Learning agent with controlled arguments. No operation takes a file path: a
7
+ // Curate agent with controlled arguments. No operation takes a file path: a
8
8
  // file's content arrives on stdin (json-parse redirects it), and the learning ops
9
9
  // build every path from the current directory.
10
10
  // Usage: node json-helper.cjs <operation> [args...]
@@ -12,6 +12,7 @@
12
12
  // Operations:
13
13
  // get-field <field> [default] Read field from stdin JSON
14
14
  // get-string-field <field> Read field from stdin JSON only when it is a string
15
+ // array-length <field> Count of an array field in stdin JSON; `unknown` for any other type
15
16
  // extract-cwd-field <field> Extract cwd + arbitrary field, SOH-byte delimited
16
17
  // session-output <context> [message] Build SessionStart output envelope (+ systemMessage)
17
18
  // prompt-output <context> Build UserPromptSubmit output envelope
@@ -280,6 +281,16 @@ try {
280
281
  break;
281
282
  }
282
283
 
284
+ case 'array-length': {
285
+ // The count of an array field: digits for an array, the word `unknown` for an
286
+ // absent field or any other type, so a caller can tell none from not reported.
287
+ // jq's `length` on an array agrees (json-parse json_array_length_file).
288
+ const input = JSON.parse(readStdin());
289
+ const val = getNestedField(input, args[0]);
290
+ console.log(Array.isArray(val) ? String(val.length) : 'unknown');
291
+ break;
292
+ }
293
+
283
294
  case 'extract-cwd-field': {
284
295
  // Extract cwd and an arbitrary top-level field from hook JSON in one pass.
285
296
  // Outputs: cwd + ASCII SOH (0x01) + field value (no trailing newline).
@@ -69,6 +69,21 @@ json_string_field_file() {
69
69
  fi
70
70
  }
71
71
 
72
+ # The element count of an array field in a JSON file. Usage: json_array_length_file "/path/to/file.json" "field"
73
+ # Prints the count as digits when the field is an array, and the word `unknown` for an
74
+ # absent field or any other type (a number, string, object, boolean or null), so a
75
+ # caller can tell "none" from "not reported". Prints nothing, and fails, when the file
76
+ # cannot be read or parsed. Silent on stderr and read the same way on both backends
77
+ # (the node path reads the file on stdin, with its stderr redirected first).
78
+ json_array_length_file() {
79
+ local file="$1" field="$2"
80
+ if [ "$_HAS_JQ" = "true" ]; then
81
+ jq -r "(try .$field catch null) | if type == \"array\" then length else \"unknown\" end" "$file" 2>/dev/null
82
+ else
83
+ node "$_JSON_HELPER" array-length "$field" 2>/dev/null < "$file"
84
+ fi
85
+ }
86
+
72
87
  # --- Multi-field batched extraction ---
73
88
 
74
89
  # Extract cwd and an arbitrary top-level field from stdin JSON in a single
@@ -103,9 +118,9 @@ json_extract_cwd_prompt() {
103
118
  # D-SYSTEMMESSAGE-ENVELOPE: the optional second argument is a message shown to the
104
119
  # user, carried in the top-level `systemMessage` key beside (never inside)
105
120
  # `hookSpecificOutput`; it does not enter the model's context. With ONE argument the
106
- # envelope is exactly what every existing caller has always received, byte for byte
107
- # (ensure-proxy, session-start-orchestrator, session-start-memory and Sections 1-5 of
108
- # session-start-context pass one). With two:
121
+ # envelope holds the context alone, byte for byte (ensure-proxy and
122
+ # session-start-orchestrator pass one; session-start-context and session-start-memory
123
+ # pass two). With two:
109
124
  # - a message and a context -> both keys, hookSpecificOutput first;
110
125
  # - a message and no context -> `systemMessage` alone, with NO hookSpecificOutput key;
111
126
  # - an empty message -> the one-argument envelope for that context.
@@ -1516,7 +1516,7 @@ function moveQueueToClaim(queuePath, claimPath) {
1516
1516
  }
1517
1517
 
1518
1518
  /**
1519
- * Claim the learning queue for one Learning run.
1519
+ * Claim the learning queue for one Curate run.
1520
1520
  *
1521
1521
  * D-OWNED-CLAIM: the learning queue is claimed and released only through the
1522
1522
  * claim-queue and release-claim ops, under the learning lock. A claim moves the
@@ -57,7 +57,7 @@ function getLearningPendingTurnsPath(projectRoot) {
57
57
  return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.jsonl');
58
58
  }
59
59
 
60
- /** .devflow/learning/.pending-turns.processing — the claimed batch a Learning run holds while it processes it */
60
+ /** .devflow/learning/.pending-turns.processing — the claimed batch a Curate run holds while it processes it */
61
61
  function getLearningPendingTurnsProcessingPath(projectRoot) {
62
62
  return path.join(projectRoot, '.devflow', 'learning', '.pending-turns.processing');
63
63
  }
@@ -81,8 +81,8 @@ function getPitfallsFilePath(projectRoot) {
81
81
  return path.join(projectRoot, '.devflow', 'learning', 'pitfalls.md');
82
82
  }
83
83
 
84
- /** .devflow/learning/learning.json — project-level learning agent tuning config */
85
- function getLearningTuningConfigPath(projectRoot) {
84
+ /** .devflow/learning/learning.json — project-level curate agent tuning config */
85
+ function getCurateTuningConfigPath(projectRoot) {
86
86
  return path.join(projectRoot, '.devflow', 'learning', 'learning.json');
87
87
  }
88
88
 
@@ -179,7 +179,7 @@ module.exports = {
179
179
  // Learning content files
180
180
  getDecisionsFilePath,
181
181
  getPitfallsFilePath,
182
- getLearningTuningConfigPath,
182
+ getCurateTuningConfigPath,
183
183
  getDecisionsLedgerPath,
184
184
  getDecisionsLogPath,
185
185
  getDecisionsArchivePath,
@@ -0,0 +1,358 @@
1
+ #!/bin/bash
2
+
3
+ # memory-permission — the memory worker's write permission, as one sourced helper.
4
+ #
5
+ # background-memory-update starts `claude -p` in a permission mode that denies every
6
+ # call no rule allows, with an allow rule for its staged file (WORKING-MEMORY.md.new),
7
+ # the only path the worker's write may reach, and a deny rule for every path under it.
8
+ # Allow rules in the user's own settings also apply to the run.
9
+ # This helper holds what the worker and session-start-memory share, so one bash 3.2 test
10
+ # run covers it (tests/shell-hooks-helpers.test.ts):
11
+ # path_occupied whether a file, a folder or a link, even a dangling one, stands on a path
12
+ # staged_path_safe whether a staged path can sit in those rules
13
+ # memory_settings_json the settings JSON that carries the rules
14
+ # memory_staged_remove the guarded removal of a folder or a link at the staged path
15
+ # memory_result_summary the fixed log line for a captured JSON result
16
+ # memory_notice_* the one-time notice: the worker writes it, a session start reads it
17
+ # memory_denied_seen the count of denied runs that raises the `denied` notice
18
+ #
19
+ # Source it after resolve-project-root (it needs df_no_symlink_below and df_file_below)
20
+ # and, for memory_result_summary, after json-parse. log() is used when the caller has one.
21
+ #
22
+ # Errexit safety: both hooks run under `set -e`, and a helper's last non-zero status
23
+ # aborts the caller. So each helper ends in an explicit `return 0` or a `return` with the
24
+ # status it means, link checks are predicates inside `if`, and a create that noclobber may
25
+ # refuse ends in `|| true`. Callers call a predicate inside an `if` or an `||` list.
26
+ #
27
+ # Character sets are spelled out in the patterns below: a range such as a-z collates by
28
+ # locale on bash 3.2.
29
+
30
+ # The reasons a notice can carry, and the file each one uses beside the memory files:
31
+ # path the project path cannot carry the rule, or the staged path is unusable
32
+ # denied Claude Code refused the worker's write in two runs with no successful update between them
33
+ MEMORY_NOTICE_REASONS="path denied"
34
+
35
+ # path_occupied <path> — returns 0 when a file, a folder or a link stands on <path>, and 1
36
+ # when nothing does. A link counts whatever it names, a dangling link included: the name is
37
+ # taken, and a create or a move onto it is not safe. Call it inside an `if` or an `||` list.
38
+ path_occupied() {
39
+ if [ -e "${1:-}" ] || [ -L "${1:-}" ]; then
40
+ return 0
41
+ fi
42
+ return 1
43
+ }
44
+
45
+ # staged_path_safe <path> — returns 0 when <path> can sit in the Edit rules as it is.
46
+ # Sets STAGED_PATH_REFUSAL to "" when it can, `chars` when the path holds a character or
47
+ # a segment the rules cannot carry, and `protected` when a .git folder lies above it (the
48
+ # worker skips such a project and logs the reason). A .claude folder above it is fine: a
49
+ # checkout in .claude/worktrees gets its working memory updated. The rule text carries no
50
+ # quoting, so a character that has a meaning in a rule path is refused rather than quoted:
51
+ # * ? [ ] \ " { } and every control character, including newline, tab and DEL. A path that
52
+ # is not absolute, starts with //, or holds an empty, . or .. segment is refused too. ( and )
53
+ # are carried as they are. The path reaches a JSON string by plain concatenation, which is
54
+ # safe after this check: $, backtick and % are inert there, and " \ and controls cannot occur.
55
+ staged_path_safe() {
56
+ local _p="${1:-}"
57
+ STAGED_PATH_REFUSAL=""
58
+ case "$_p" in
59
+ /*) ;;
60
+ *) STAGED_PATH_REFUSAL="chars"; return 1 ;;
61
+ esac
62
+ case "$_p" in
63
+ //*|*//*|*/./*|*/../*|*/.|*/..) STAGED_PATH_REFUSAL="chars"; return 1 ;;
64
+ esac
65
+ case "$_p" in
66
+ *[[:cntrl:]]*) STAGED_PATH_REFUSAL="chars"; return 1 ;;
67
+ esac
68
+ case "$_p" in
69
+ *'*'*|*'?'*|*'['*|*']'*|*'\'*|*'"'*|*'{'*|*'}'*) STAGED_PATH_REFUSAL="chars"; return 1 ;;
70
+ esac
71
+ case "$_p" in
72
+ */.git/*) STAGED_PATH_REFUSAL="protected"; return 1 ;;
73
+ esac
74
+ return 0
75
+ }
76
+
77
+ # memory_settings_json <staged path> — prints the settings JSON for `claude --settings`:
78
+ # two rules and no hooks (// makes a rule path absolute):
79
+ # allow Edit(//<staged>) the staged file
80
+ # deny Edit(//<staged>/*) every path under the staged path
81
+ # Under dontAsk, any call no rule allows is refused, so the worker's write is limited to
82
+ # its staged file and to what allow rules in the user's own settings cover.
83
+ # Measured on Claude Code 2.1.269, 2.1.280, 2.1.286 and 2.1.299, both argv forms,
84
+ # against a local stand-in for the Messages API (2026-10-15): with these two rules
85
+ # the Write of the staged file succeeds, and a Write to <staged>/x, <staged>/a/b,
86
+ # <staged>/a/b/c/d/e, <staged>/.hidden or <staged>/../<name> is refused. The deny is
87
+ # `/*` and not `/**`: `/**` was measured to refuse the staged file too (2.1.269 and
88
+ # 2.1.299, 0 of 3 writes), and then no memory is written. A path with two pairs of ( ) in it
89
+ # (`obs ( )/proj (copy)`) carries both rules as they are: on 2.1.269 with the legacy argv
90
+ # and on 2.1.299 with the lean argv, the Write of the staged file succeeded and a Write to
91
+ # <staged>/x, <staged>/a/b, <staged>/.hidden, <staged>/../<name> and to a file in the
92
+ # project root was refused. Prints nothing and returns 1 for a path staged_path_safe
93
+ # refuses, so a rule is never built for an unchecked path.
94
+ memory_settings_json() {
95
+ local _p="${1:-}"
96
+ staged_path_safe "$_p" || return 1
97
+ printf '%s' '{"permissions":{"allow":["Edit(/'"$_p"')"],"deny":["Edit(/'"$_p"'/*)"]},"disableAllHooks":true}'
98
+ return 0
99
+ }
100
+
101
+ # STAGED_REMOVE_MAX_LISTING — the most lines `ls -RA` may print for a folder at the staged
102
+ # path. A folder with a longer listing is not deleted.
103
+ STAGED_REMOVE_MAX_LISTING=256
104
+
105
+ # memory_staged_remove <memory dir> <root> <staged path> — makes the staged path empty
106
+ # when a folder or a link stands on it, with one delete of exactly that path. <root> is
107
+ # the project root as it resolves on disk. It deletes only when it can prove all of these,
108
+ # and otherwise deletes nothing, returns 1 and sets STAGED_REMOVE_REFUSAL to the keyword
109
+ # of the first fact it could not prove:
110
+ # args <memory dir>, <root> and <staged path> are all set, and both paths are absolute
111
+ # path <staged path> is exactly <memory dir>/WORKING-MEMORY.md.new
112
+ # parent <memory dir> is <root>/.devflow/memory, as a string and then on disk
113
+ # link no link sits on the way down from <root> to <memory dir>
114
+ # size a folder lists at most STAGED_REMOVE_MAX_LISTING lines of `ls -RA`
115
+ # rm the delete took effect: nothing stands on the staged path afterwards
116
+ # A link is removed as the link (`rm -f`, never `-r`), so what it names is never touched;
117
+ # a folder is removed with `rm -rf`, which unlinks a link inside it and never follows one.
118
+ # The path is a single quoted word, so no pattern expands. Sets STAGED_REMOVED to `link`,
119
+ # `dir`, or `none` (nothing removed). A regular file, or nothing, is left as it is and
120
+ # returns 0: the caller removes a stale staged file itself. Returns 0 when no folder and
121
+ # no link stands on the path afterwards. The checks and the delete are separate steps, so
122
+ # a process that swaps something in between is not stopped; such a process already runs as
123
+ # the user.
124
+ memory_staged_remove() {
125
+ local _dir="${1:-}" _root="${2:-}" _staged="${3:-}" _kind="" _phys="" _n=0
126
+ STAGED_REMOVED="none"
127
+ STAGED_REMOVE_REFUSAL=""
128
+ case "$_dir" in /?*) ;; *) STAGED_REMOVE_REFUSAL="args"; return 1 ;; esac
129
+ case "$_root" in /?*) ;; *) STAGED_REMOVE_REFUSAL="args"; return 1 ;; esac
130
+ case "$_staged" in /?*) ;; *) STAGED_REMOVE_REFUSAL="args"; return 1 ;; esac
131
+ if [ -L "$_staged" ]; then
132
+ _kind="link"
133
+ elif [ -d "$_staged" ]; then
134
+ _kind="dir"
135
+ else
136
+ return 0
137
+ fi
138
+ if [ "$_staged" != "$_dir/WORKING-MEMORY.md.new" ]; then
139
+ STAGED_REMOVE_REFUSAL="path"; return 1
140
+ fi
141
+ if [ "$_dir" != "$_root/.devflow/memory" ]; then
142
+ STAGED_REMOVE_REFUSAL="parent"; return 1
143
+ fi
144
+ if ! df_no_symlink_below "$_root" "$_dir"; then
145
+ STAGED_REMOVE_REFUSAL="link"; return 1
146
+ fi
147
+ _phys=$(cd -P -- "$_dir" 2>/dev/null && pwd -P) || _phys=""
148
+ if [ "$_phys" != "$_root/.devflow/memory" ]; then
149
+ STAGED_REMOVE_REFUSAL="parent"; return 1
150
+ fi
151
+ if [ "$_kind" = "dir" ]; then
152
+ _n=$(ls -RA -- "$_staged" 2>/dev/null | head -n $((STAGED_REMOVE_MAX_LISTING + 1)) | wc -l) || _n=0
153
+ _n=$((_n + 0))
154
+ if [ "$_n" -gt "$STAGED_REMOVE_MAX_LISTING" ]; then
155
+ STAGED_REMOVE_REFUSAL="size"; return 1
156
+ fi
157
+ rm -rf -- "$_staged" 2>/dev/null || true
158
+ else
159
+ rm -f -- "$_staged" 2>/dev/null || true
160
+ fi
161
+ if path_occupied "$_staged"; then
162
+ STAGED_REMOVE_REFUSAL="rm"; return 1
163
+ fi
164
+ STAGED_REMOVED="$_kind"
165
+ return 0
166
+ }
167
+
168
+ # The most characters a result field may carry into the log line. A longer value reads
169
+ # `unknown` (a status code is left out).
170
+ MEMORY_RESULT_DENIALS_MAX_DIGITS=6
171
+ MEMORY_RESULT_TURNS_MAX_DIGITS=6
172
+ MEMORY_RESULT_STATUS_MAX_DIGITS=3
173
+ MEMORY_RESULT_SUBTYPE_MAX_CHARS=40
174
+
175
+ # _memory_field_within <digits|letters> <value> <max> — sets _MEMORY_FIELD to <value> when
176
+ # it is one or more characters of the named set and at most <max> of them, and to `unknown`
177
+ # otherwise. `digits` is 0-9; `letters` is a-z and _. Both are spelled out, since a range
178
+ # collates by locale on bash 3.2. A builtin only, so it forks nothing. Always returns 0.
179
+ _memory_field_within() {
180
+ local _set="${1:-}" _value="${2:-}" _max="${3:-0}"
181
+ _MEMORY_FIELD="unknown"
182
+ case "$_set" in
183
+ digits)
184
+ case "$_value" in ''|*[!0123456789]*) return 0 ;; esac
185
+ ;;
186
+ letters)
187
+ case "$_value" in ''|*[!abcdefghijklmnopqrstuvwxyz_]*) return 0 ;; esac
188
+ ;;
189
+ *) return 0 ;;
190
+ esac
191
+ if [ "${#_value}" -le "$_max" ]; then _MEMORY_FIELD="$_value"; fi
192
+ return 0
193
+ }
194
+
195
+ # memory_result_summary <file> — reduces a `--output-format json` result file to a fixed
196
+ # line, and reads nothing else of it: the tool input and the result text, which can hold
197
+ # the memory body, are never read. Sets, in the caller's scope:
198
+ # MEMORY_RESULT_LINE Result: <subtype>, is_error=<true|false>, <n> turn(s),
199
+ # <d> permission denial(s)[, api_status=<digits>]
200
+ # or Result: not reported (empty|unparseable|no parser)
201
+ # MEMORY_RESULT_DENIALS the denial count as digits, or `unknown` when the result does
202
+ # not report one (an absent or non-array field is never 0)
203
+ # Every field is checked against its own shape, so text from the file cannot enter the
204
+ # line: a field that fails its check reads `unknown`. Always returns 0.
205
+ memory_result_summary() {
206
+ local _f="${1:-}" _sub="" _err="" _turns="" _den="" _status="" _api=""
207
+ MEMORY_RESULT_LINE=""
208
+ MEMORY_RESULT_DENIALS="unknown"
209
+ if [ "${_JSON_AVAILABLE:-false}" != "true" ]; then
210
+ MEMORY_RESULT_LINE="Result: not reported (no parser)"
211
+ return 0
212
+ fi
213
+ if [ -z "$_f" ] || [ ! -s "$_f" ]; then
214
+ MEMORY_RESULT_LINE="Result: not reported (empty)"
215
+ return 0
216
+ fi
217
+ _den=$(json_array_length_file "$_f" "permission_denials") || _den=""
218
+ if [ -z "$_den" ]; then
219
+ MEMORY_RESULT_LINE="Result: not reported (unparseable)"
220
+ return 0
221
+ fi
222
+ _memory_field_within digits "$_den" "$MEMORY_RESULT_DENIALS_MAX_DIGITS"; _den="$_MEMORY_FIELD"
223
+ _sub=$(json_string_field_file "$_f" "subtype") || _sub=""
224
+ _memory_field_within letters "$_sub" "$MEMORY_RESULT_SUBTYPE_MAX_CHARS"; _sub="$_MEMORY_FIELD"
225
+ _err=$(json_field_file "$_f" "is_error" "") || _err=""
226
+ case "$_err" in
227
+ true|false) ;;
228
+ *) _err="unknown" ;;
229
+ esac
230
+ _turns=$(json_field_file "$_f" "num_turns" "") || _turns=""
231
+ _memory_field_within digits "$_turns" "$MEMORY_RESULT_TURNS_MAX_DIGITS"; _turns="$_MEMORY_FIELD"
232
+ _status=$(json_field_file "$_f" "api_error_status" "") || _status=""
233
+ _memory_field_within digits "$_status" "$MEMORY_RESULT_STATUS_MAX_DIGITS"
234
+ if [ "$_MEMORY_FIELD" != "unknown" ]; then _api=", api_status=${_MEMORY_FIELD}"; fi
235
+ MEMORY_RESULT_LINE="Result: ${_sub}, is_error=${_err}, ${_turns} turn(s), ${_den} permission denial(s)${_api}"
236
+ MEMORY_RESULT_DENIALS="$_den"
237
+ return 0
238
+ }
239
+
240
+ # memory_notice_write <memory dir> <root> <reason> — leaves a notice for the next session
241
+ # start, once per reason: only where neither the notice nor its `.shown` marker stands,
242
+ # and where no link sits on the way down from <root>. The builtin tests come first, since
243
+ # noclobber alone opens a link to a FIFO or a device; noclobber then makes the create
244
+ # itself exclusive. Content: the reason. Best effort: a refusal leaves nothing.
245
+ memory_notice_write() {
246
+ local _dir="${1:-}" _root="${2:-}" _reason="${3:-}" _file
247
+ case " $MEMORY_NOTICE_REASONS " in
248
+ *" $_reason "*) ;;
249
+ *) return 0 ;;
250
+ esac
251
+ [ -d "$_dir" ] || return 0
252
+ _file="$_dir/.worker-notice-$_reason"
253
+ if ! path_occupied "$_file" && ! path_occupied "$_file.shown" \
254
+ && df_no_symlink_below "$_root" "$_file" "$_file.shown"; then
255
+ ( umask 077; set -C; printf '%s\n' "$_reason" > "$_file" ) 2>/dev/null || true
256
+ fi
257
+ return 0
258
+ }
259
+
260
+ # memory_notice_clear <memory dir> <root> <reason> — removes the notice, its `.shown`
261
+ # marker and, for `denied`, the one-denied-run marker, once the cause is gone. The
262
+ # files are removed only where no link sits on the way down from <root>.
263
+ memory_notice_clear() {
264
+ local _dir="${1:-}" _root="${2:-}" _reason="${3:-}" _file _mark
265
+ case " $MEMORY_NOTICE_REASONS " in
266
+ *" $_reason "*) ;;
267
+ *) return 0 ;;
268
+ esac
269
+ _file="$_dir/.worker-notice-$_reason"
270
+ _mark="$_dir/.worker-denied-once"
271
+ if path_occupied "$_file" || path_occupied "$_file.shown"; then
272
+ if df_no_symlink_below "$_root" "$_file" "$_file.shown"; then
273
+ rm -f "$_file" "$_file.shown" 2>/dev/null || true
274
+ fi
275
+ fi
276
+ if [ "$_reason" = "denied" ] && path_occupied "$_mark"; then
277
+ if df_no_symlink_below "$_root" "$_mark"; then
278
+ rm -f "$_mark" 2>/dev/null || true
279
+ fi
280
+ fi
281
+ return 0
282
+ }
283
+
284
+ # memory_denied_seen <memory dir> <root> — records one run that Claude Code refused the
285
+ # worker's write in. The first leaves a marker; a second, with the marker still there,
286
+ # raises the `denied` notice. A run that swapped the memory in clears both
287
+ # (memory_notice_clear denied), so only two denied runs with no successful update between
288
+ # them count.
289
+ memory_denied_seen() {
290
+ local _dir="${1:-}" _root="${2:-}" _mark
291
+ [ -d "$_dir" ] || return 0
292
+ _mark="$_dir/.worker-denied-once"
293
+ if path_occupied "$_mark"; then
294
+ memory_notice_write "$_dir" "$_root" "denied"
295
+ elif df_no_symlink_below "$_root" "$_mark"; then
296
+ ( umask 077; set -C; : > "$_mark" ) 2>/dev/null || true
297
+ fi
298
+ return 0
299
+ }
300
+
301
+ # memory_notice_text <reason> <log file> — sets MEMORY_NOTICE_TEXT to the message for a
302
+ # reason, naming <log file>, and to "" for a reason it has no message for. Always returns 0.
303
+ memory_notice_text() {
304
+ local _reason="${1:-}" _logfile="${2:-}"
305
+ local _where="The worker log is .background-memory-update.log under ~/.devflow/logs/, in the folder for the session that ran it. For this session: ${_logfile}"
306
+ MEMORY_NOTICE_TEXT=""
307
+ case "$_reason" in
308
+ path)
309
+ MEMORY_NOTICE_TEXT="Devflow: working memory is not updating because the worker cannot write its staged file at this project path. ${_where}"
310
+ ;;
311
+ denied)
312
+ MEMORY_NOTICE_TEXT="Devflow: working memory is not updating because Claude Code refused the worker's write in two runs with no successful update between them. ${_where}"
313
+ ;;
314
+ esac
315
+ return 0
316
+ }
317
+
318
+ # memory_notice_read <memory dir> <root> <log file> — builds the message for each pending
319
+ # notice and moves each to its `.shown` marker, so a reason is shown once. Sets
320
+ # MEMORY_NOTICE_MESSAGE (empty when nothing is pending), joined by a space when more
321
+ # than one reason is pending. A notice is read only where no link sits on its path; one
322
+ # whose content does not fit its reason is moved to `.shown` unshown and logged.
323
+ # <log file> is named in the messages (memory_notice_text). Always returns 0.
324
+ memory_notice_read() {
325
+ local _dir="${1:-}" _root="${2:-}" _logfile="${3:-}"
326
+ local _reason _file _line _tag _extra _text
327
+ MEMORY_NOTICE_MESSAGE=""
328
+ for _reason in $MEMORY_NOTICE_REASONS; do
329
+ _file="$_dir/.worker-notice-$_reason"
330
+ df_file_below "$_root" "$_file" || continue
331
+ _line=""; _tag=""; _extra=""; _text=""
332
+ # stderr is redirected before stdin, so a notice that a concurrent session start moved
333
+ # away since the check above fails the open in silence.
334
+ IFS= read -r -n 200 _line 2>/dev/null < "$_file" || true
335
+ read -r _tag _extra <<< "$_line" || true
336
+ if [ "$_tag" = "$_reason" ] && [ -z "$_extra" ]; then
337
+ memory_notice_text "$_reason" "$_logfile"
338
+ _text="$MEMORY_NOTICE_TEXT"
339
+ fi
340
+ if [ -n "$_text" ]; then
341
+ if [ -n "$MEMORY_NOTICE_MESSAGE" ]; then
342
+ MEMORY_NOTICE_MESSAGE="${MEMORY_NOTICE_MESSAGE} ${_text}"
343
+ else
344
+ MEMORY_NOTICE_MESSAGE="$_text"
345
+ fi
346
+ elif declare -F log >/dev/null 2>&1; then
347
+ log "Worker notice for reason ${_reason} ignored: its content does not fit the reason"
348
+ fi
349
+ if df_no_symlink_below "$_root" "$_file" "$_file.shown"; then
350
+ if ! path_occupied "$_file.shown"; then
351
+ mv "$_file" "$_file.shown" 2>/dev/null || true
352
+ else
353
+ rm -f "$_file" 2>/dev/null || true
354
+ fi
355
+ fi
356
+ done
357
+ return 0
358
+ }
@@ -75,7 +75,7 @@ elif [[ "$HEAD" == "/"* ]]; then
75
75
  dbg "EXIT: slash command — no reminder"
76
76
  else
77
77
  dbg "ORCHESTRATOR_REMINDER injected"
78
- json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds, multi-file reads, and debug loops to the fitting roster agent (Agent tool) or the matching devflow workflow skill.
78
+ json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to the fitting roster agent (Agent tool) or the matching devflow workflow skill. Send debug work to the devflow:debug skill, never to the Debug agent.
79
79
  Keep only judgment work mainline: conversation, decisions, routing, synthesis of agent reports."
80
80
  fi
81
81