devflow-kit 2.4.0 → 3.0.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.
- package/CHANGELOG.md +229 -0
- package/README.md +111 -18
- package/dist/agents/git.md +822 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +48 -55
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +61 -27
- package/dist/cli/commands/init.js +649 -240
- package/dist/cli/commands/install-report.js +200 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +35 -37
- package/dist/cli/commands/learning.js +79 -57
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +134 -135
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +81 -29
- package/dist/cli/commands/skills.js +71 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +277 -0
- package/dist/cli/commands/uninstall.js +520 -169
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +58 -14
- package/dist/commands/code-review.md +110 -32
- package/dist/commands/debug.md +55 -11
- package/dist/commands/dynamic-build.md +344 -73
- package/dist/commands/dynamic-plan.md +77 -27
- package/dist/commands/dynamic-profile.md +25 -11
- package/dist/commands/dynamic-tickets.md +76 -15
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +314 -62
- package/dist/commands/plan.md +146 -32
- package/dist/commands/release.md +64 -17
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +196 -68
- package/dist/commands/self-review.md +45 -9
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +363 -0
- package/dist/core/feature-config.js +200 -65
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +34 -6
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +38 -10
- package/dist/core/mds-variants.js +798 -0
- package/dist/core/migrations.js +49 -23
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +361 -12
- package/dist/core/project-paths.js +1 -18
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +494 -0
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +1187 -32
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +366 -151
- package/dist/targets/claude-code/tracker-install.js +134 -0
- package/package.json +8 -6
- package/src/assets/agents/code.md +45 -6
- package/src/assets/agents/design.md +2 -1
- package/src/assets/agents/git.mds +825 -0
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +474 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +13 -11
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +2 -2
- package/src/assets/commands/_partials/_publication.mds +8 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +31 -19
- package/src/assets/commands/code-review.mds +67 -41
- package/src/assets/commands/debug.mds +13 -7
- package/src/assets/commands/dynamic-build.mds +274 -66
- package/src/assets/commands/dynamic-plan.mds +50 -23
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +63 -16
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +234 -67
- package/src/assets/commands/plan.mds +91 -33
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +150 -78
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +305 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +40 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -8
- package/src/assets/scripts/hooks/capture-question +18 -8
- package/src/assets/scripts/hooks/capture-turn +27 -13
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +228 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +22 -13
- package/src/assets/scripts/hooks/pre-compact-memory +44 -15
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +146 -28
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +534 -20
- package/src/assets/scripts/hooks/session-start-memory +38 -15
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +4 -2
- package/src/assets/skills/docs-framework/SKILL.md +11 -10
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
- package/src/assets/agents/git.md +0 -938
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# SessionStart Hook: Cross-Feature Context Injection
|
|
4
4
|
# Always-on hook that injects learning context as additionalContext. Sections
|
|
5
|
-
# are gated by the `learning`
|
|
6
|
-
# this
|
|
5
|
+
# 1–2 are gated by the `features.learning` switch in ~/.devflow/manifest.json,
|
|
6
|
+
# which this checkout's project.json / config.json can only narrow
|
|
7
|
+
# (D-FEATURES-NARROW-ONLY) — this hook itself is never disabled. They and the .gitignore carve-out run only in a git project that is
|
|
8
|
+
# not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
|
|
9
|
+
# the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
|
|
7
10
|
#
|
|
8
11
|
# Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines).
|
|
9
12
|
# Section 2: Learning maintenance directive — when captured turns are pending in
|
|
@@ -11,6 +14,14 @@
|
|
|
11
14
|
# the main model to spawn the background Learning agent with the resolved model.
|
|
12
15
|
# The agent claims the queue itself and queue emptiness is the natural gate,
|
|
13
16
|
# so there is no throttle here.
|
|
17
|
+
# Section 3: Tracker setup directive — when the issue tracker in effect for this
|
|
18
|
+
# project is not GitHub and no ~/.devflow/tracker/{provider}.md has been inferred
|
|
19
|
+
# for it yet, instructs the main model to spawn the background Tracker agent. The
|
|
20
|
+
# machine provider comes from a sentinel read with a builtin, so a GitHub user —
|
|
21
|
+
# and a jira machine whose conventions are learned — forks nothing.
|
|
22
|
+
# Section 4: Legacy install notice — one line when this repository still carries
|
|
23
|
+
# a retired project-local devflow install (its .claude/settings.json registers
|
|
24
|
+
# devflow hooks), pointing at `devflow uninstall --scope local`.
|
|
14
25
|
|
|
15
26
|
# Safe no-op fallback: must exist before hook-bootstrap is sourced.
|
|
16
27
|
dbg() { :; }
|
|
@@ -47,37 +58,147 @@ devflow_debug_set_cwd "$CWD"
|
|
|
47
58
|
dbg "CWD=$CWD"
|
|
48
59
|
|
|
49
60
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
|
|
50
|
-
# hook runs with a CWD inside .devflow/...).
|
|
61
|
+
# hook runs with a CWD inside .devflow/...). One git call yields both roots
|
|
62
|
+
# (resolve-project-root): PROJECT_ROOT is this checkout's toplevel, LEDGER_ROOT the
|
|
63
|
+
# main worktree when this is a linked worktree whose main checkout already has a
|
|
64
|
+
# .devflow (D-LEDGER-MAIN-WORKTREE). Empty → fall back to CWD.
|
|
51
65
|
source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
df_resolve_roots "$CWD" 2>/dev/null || true
|
|
67
|
+
PROJECT_ROOT="${DF_ROOT:-$CWD}"
|
|
68
|
+
LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
|
|
69
|
+
|
|
70
|
+
# D-HOOKS-GIT-ONLY (git-marker): per-project work — the .gitignore carve-out and
|
|
71
|
+
# Sections 1–2 — happens only for a git project that is not the home directory.
|
|
72
|
+
# Outside one there is no repository for the carve-out to protect, so writing it
|
|
73
|
+
# would litter whatever directory Claude Code was launched from, and at `~` the
|
|
74
|
+
# project data would land inside the machine root. df_is_project_root is two
|
|
75
|
+
# builtin walks, so this gate forks nothing. Section 3 keeps its own marker gate below: tracker
|
|
76
|
+
# setup is machine-scoped and is not project scaffolding.
|
|
77
|
+
source "$SCRIPT_DIR/git-marker" 2>/dev/null || true
|
|
78
|
+
PROJECT_OK=""
|
|
79
|
+
if df_is_project_root "$PROJECT_ROOT" 2>/dev/null; then
|
|
80
|
+
PROJECT_OK="yes"
|
|
81
|
+
else
|
|
82
|
+
dbg "project work skipped: not a git project, or the project root is HOME"
|
|
83
|
+
fi
|
|
54
84
|
|
|
55
85
|
# Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
|
|
56
|
-
# session regardless of feature toggles, so memory-off projects
|
|
57
|
-
# only) still get .devflow/ ignored — this is the
|
|
58
|
-
# the gitignore/memory coupling (PF-014). Single
|
|
59
|
-
# Soft-fail: a gitignore write must never
|
|
60
|
-
|
|
86
|
+
# git-project session regardless of feature toggles, so memory-off projects
|
|
87
|
+
# (learning/knowledge only) still get .devflow/ ignored — this is the
|
|
88
|
+
# memory-independent path that fixes the gitignore/memory coupling (PF-014). Single
|
|
89
|
+
# source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
|
|
90
|
+
# block context injection. Marker keeps it O(1).
|
|
91
|
+
[ -n "$PROJECT_OK" ] && [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ] && source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
|
|
61
92
|
|
|
62
93
|
CONTEXT=""
|
|
63
94
|
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
# The devflow machine root, always $HOME/.devflow (D-ONE-HOME): Section 3's
|
|
96
|
+
# tracker files and the manifest the learning gate reads are machine-wide, not
|
|
97
|
+
# project-scoped (the project's own .devflow is PROJECT_DEVFLOW_DIR below).
|
|
98
|
+
TRACKER_DEVFLOW_DIR="$HOME/.devflow"
|
|
99
|
+
|
|
100
|
+
# The shape gate for every path this hook interpolates into a directive.
|
|
101
|
+
#
|
|
102
|
+
# Section 2 embeds $LEDGER_ROOT; Section 3 embeds $PROJECT_ROOT and
|
|
103
|
+
# $TRACKER_DEVFLOW_DIR — each inside a double-quoted `prompt: "..."` string the
|
|
104
|
+
# model reads out of additionalContext. The provider and model TOKENS in those
|
|
105
|
+
# directives are admitted by positive allowlists, and so are these: the gate is the
|
|
106
|
+
# POSITIVE shape ^[A-Za-z0-9/._+-]+$, expressed as "rejects if any character falls
|
|
107
|
+
# outside it".
|
|
108
|
+
#
|
|
109
|
+
# Positive, not a denylist of the characters that are known to hurt. A denylist
|
|
110
|
+
# enumerates the injections someone thought of — `"` closes the prompt string, `\`
|
|
111
|
+
# reads as an escape, LF and CR put the rest of the path on its own line as free
|
|
112
|
+
# text — and admits every one that was not on the list. An allowlist admits only
|
|
113
|
+
# what is known to be inert, so the next escape nobody has thought of is refused
|
|
114
|
+
# by construction rather than by a later amendment.
|
|
115
|
+
#
|
|
116
|
+
# The narrowing is real and deliberate: a project root containing a space, a
|
|
117
|
+
# quote, a backtick, `$` or `;` suppresses the directive that embeds it rather
|
|
118
|
+
# than interpolating an unproven value. That is the fail-closed direction — the
|
|
119
|
+
# directive is an optimisation — but it must not be a SILENT one: a refused
|
|
120
|
+
# Learning root with pending work emits the fixed `--- LEARNING PAUSED ---`
|
|
121
|
+
# notice instead (Section 2), which interpolates nothing, so the user learns why
|
|
122
|
+
# the queue never drains rather than finding it capped at 200 rows one day.
|
|
123
|
+
#
|
|
124
|
+
# `+` is admitted: Claude Code names a worktree for a slash branch with it
|
|
125
|
+
# (`feat/x` → `feat+x`), so refusing it would silence learning in exactly the
|
|
126
|
+
# linked worktrees users open most, and it is inert inside a double-quoted prompt
|
|
127
|
+
# string — it closes nothing, escapes nothing and expands nothing.
|
|
128
|
+
#
|
|
129
|
+
# What the range does NOT promise is "ASCII only". A `case` bracket range
|
|
130
|
+
# collates under LC_COLLATE, so `A-Za-z0-9` admits an accented letter under a
|
|
131
|
+
# UTF-8 locale and refuses it under C. Every byte the gate exists to refuse —
|
|
132
|
+
# quote, backslash, CR, LF, space, backtick, `$`, `;` — is outside the range in
|
|
133
|
+
# both, so the security property holds either way; only the exact width of the
|
|
134
|
+
# admitted set is locale-dependent, and no rule here rests on it.
|
|
135
|
+
#
|
|
136
|
+
# Checked ONCE, here, where every value is resolved and above every section that
|
|
137
|
+
# interpolates them, so no sink can embed a value no gate saw (PF-023 — the
|
|
138
|
+
# invariant belongs at the convergence point all callers pass through, not in
|
|
139
|
+
# whichever section someone remembered). `case` is a shell builtin, so the GitHub
|
|
140
|
+
# path still forks zero times [DR-10]. The empty arm is explicit: an unset root
|
|
141
|
+
# must not read as "no forbidden character, therefore safe".
|
|
142
|
+
#
|
|
143
|
+
# ONE gate per VALUE, applied to the root each section embeds, not one over their
|
|
144
|
+
# concatenation. Section 2 interpolates $LEDGER_ROOT alone — the main worktree's
|
|
145
|
+
# root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value checked
|
|
146
|
+
# for that section, not the checkout's own; only Section 3 interpolates
|
|
147
|
+
# $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
|
|
148
|
+
# ~/.devflow shape suppress the Learning directive as well — a value Section 2
|
|
149
|
+
# never embeds, silently disabling the whole learning pipeline for any machine
|
|
150
|
+
# whose home directory carries a space. A gate must refuse a sink its value
|
|
151
|
+
# actually reaches and no other, or the fail-closed direction stops being the
|
|
152
|
+
# safe one.
|
|
153
|
+
DIRECTIVE_LEDGER_SAFE="yes"
|
|
154
|
+
case "$LEDGER_ROOT" in
|
|
155
|
+
''|*[!A-Za-z0-9/._+-]*)
|
|
156
|
+
DIRECTIVE_LEDGER_SAFE=""
|
|
157
|
+
;;
|
|
158
|
+
esac
|
|
159
|
+
|
|
160
|
+
DIRECTIVE_ROOT_SAFE="yes"
|
|
161
|
+
case "$PROJECT_ROOT" in
|
|
162
|
+
''|*[!A-Za-z0-9/._+-]*)
|
|
163
|
+
DIRECTIVE_ROOT_SAFE=""
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
|
|
167
|
+
# Section 3's flag: BOTH of its values, because Section 3 interpolates both.
|
|
168
|
+
# Seeded from the project-root flag so it can only ever be narrower — a root the
|
|
169
|
+
# section may not embed is not embeddable alongside a second path either.
|
|
170
|
+
DIRECTIVE_PATHS_SAFE="$DIRECTIVE_ROOT_SAFE"
|
|
171
|
+
case "$TRACKER_DEVFLOW_DIR" in
|
|
172
|
+
''|*[!A-Za-z0-9/._+-]*)
|
|
173
|
+
DIRECTIVE_PATHS_SAFE=""
|
|
174
|
+
;;
|
|
175
|
+
esac
|
|
176
|
+
|
|
177
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
178
|
+
LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
|
|
179
|
+
LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
|
|
66
180
|
|
|
67
181
|
# Normal logging
|
|
68
182
|
source "$SCRIPT_DIR/hook-log-init" "session-start-context"
|
|
69
183
|
|
|
70
|
-
# --- Learning gate
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
184
|
+
# --- Learning gate: the machine switch, narrowed by this checkout ---
|
|
185
|
+
# D-FEATURES-NARROW-ONLY (see queue-append): ~/.devflow/manifest.json's
|
|
186
|
+
# features.learning, narrowed by PROJECT_ROOT's project.json / config.json, is
|
|
187
|
+
# read by the same helper the capture hooks use, so the directive and the queue
|
|
188
|
+
# it drains can never disagree about whether learning is on. The manifest lives
|
|
189
|
+
# under the machine root captured above, not the project-scoped
|
|
190
|
+
# PROJECT_DEVFLOW_DIR. Neither file present, or neither holding a `false`, costs
|
|
191
|
+
# no fork (D-GATES-FAST-PATH).
|
|
192
|
+
source "$SCRIPT_DIR/queue-append" || { echo "session-start-context: failed to source queue-append" >&2; exit 1; }
|
|
193
|
+
queue_read_gates "$TRACKER_DEVFLOW_DIR/manifest.json" "$PROJECT_ROOT"
|
|
194
|
+
LEARNING_ENABLED="$_QG_LEARNING"
|
|
195
|
+
# Learning is project work: off outside a git project (D-HOOKS-GIT-ONLY above).
|
|
196
|
+
[ -n "$PROJECT_OK" ] || LEARNING_ENABLED="false"
|
|
76
197
|
|
|
77
198
|
# --- Section 1: Project Decisions TL;DR ---
|
|
78
199
|
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
79
200
|
# Heal older installs that have .devflow/ but not .devflow/learning/
|
|
80
|
-
if [ -d "$
|
|
201
|
+
if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
|
|
81
202
|
mkdir -p "$LEARNING_DIR" 2>/dev/null || true
|
|
82
203
|
fi
|
|
83
204
|
if [ -d "$LEARNING_DIR" ]; then
|
|
@@ -130,6 +251,25 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
130
251
|
LEARNING_WORK="queue"
|
|
131
252
|
fi
|
|
132
253
|
|
|
254
|
+
# $LEDGER_ROOT is the only path this directive interpolates (line below), so
|
|
255
|
+
# it is the only one whose shape may suppress it. A refusal is not silent: the
|
|
256
|
+
# fixed notice below names the pause and interpolates NOTHING — no path, no
|
|
257
|
+
# model, no count — so no value the gate refused can reach the context by it.
|
|
258
|
+
if [ -n "$LEARNING_WORK" ] && [ -z "$DIRECTIVE_LEDGER_SAFE" ]; then
|
|
259
|
+
dbg "learning directive suppressed: interpolated path shape rejected — fixed notice emitted"
|
|
260
|
+
LEARNING_WORK=""
|
|
261
|
+
LEARNING_PAUSED_SECTION='--- LEARNING PAUSED ---
|
|
262
|
+
Captured turns are waiting in the learning queue of this project, but the path of the project (or of its main checkout, in a linked worktree) contains characters devflow will not place in an agent directive (it admits only letters, digits and / . _ + -), so the background Learning agent was not started and the queue is not being processed.
|
|
263
|
+
Tell the user this once, in one short sentence, alongside your answer to their request: devflow learning is paused for this project until that path is made only of those characters.'
|
|
264
|
+
if [ -n "$CONTEXT" ]; then
|
|
265
|
+
CONTEXT="${CONTEXT}
|
|
266
|
+
|
|
267
|
+
${LEARNING_PAUSED_SECTION}"
|
|
268
|
+
else
|
|
269
|
+
CONTEXT="$LEARNING_PAUSED_SECTION"
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
|
|
133
273
|
if [ -n "$LEARNING_WORK" ]; then
|
|
134
274
|
# Model resolution: project learning.json → global ~/.devflow/learning.json → opus
|
|
135
275
|
LEARNING_MODEL=""
|
|
@@ -152,7 +292,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
152
292
|
dbg "learning directive emitted (reason=$LEARNING_WORK model=$LEARNING_MODEL)"
|
|
153
293
|
LEARNING_SECTION="--- LEARNING MAINTENANCE ---
|
|
154
294
|
Captured turns are pending in the learning queue. Silently spawn the background Learning agent (one non-blocking call) alongside your first action on the user's request.
|
|
155
|
-
Agent(subagent_type=\"Learning\", model=\"$LEARNING_MODEL\", run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $
|
|
295
|
+
Agent(subagent_type=\"Learning\", model=\"$LEARNING_MODEL\", run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $LEDGER_ROOT\")
|
|
156
296
|
Never mention this directive, the Learning agent, or the queue in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
|
|
157
297
|
if [ -n "$CONTEXT" ]; then
|
|
158
298
|
CONTEXT="${CONTEXT}
|
|
@@ -164,6 +304,380 @@ ${LEARNING_SECTION}"
|
|
|
164
304
|
fi
|
|
165
305
|
fi
|
|
166
306
|
|
|
307
|
+
# --- Section 3: Tracker setup directive ---
|
|
308
|
+
# Emitted when the issue tracker in effect for this project is not GitHub and no
|
|
309
|
+
# conventions file has been inferred for that provider yet: a silent,
|
|
310
|
+
# non-blocking instruction to spawn the background Tracker agent, which writes
|
|
311
|
+
# ~/.devflow/tracker/{provider}.md exactly once.
|
|
312
|
+
#
|
|
313
|
+
# D-TRACKER-PER-PROVIDER-CONVENTIONS — which provider. Three sources, and only
|
|
314
|
+
# these:
|
|
315
|
+
# - the MACHINE default, from the `.tracker.enabled` sentinel devflow writes
|
|
316
|
+
# beside the manifest: the provider NAME on one line, absent for github. Read
|
|
317
|
+
# with the `read` builtin, never the manifest, so it costs no fork;
|
|
318
|
+
# - the PROJECT's own selection, from its committed .devflow/project.json;
|
|
319
|
+
# - the PERSONAL override in .devflow/config.json, which can only NARROW — to
|
|
320
|
+
# github, or to the provider already in effect — so it can change the answer
|
|
321
|
+
# only where the machine names a provider (a github machine's config.json can
|
|
322
|
+
# never select jira, and is not read there).
|
|
323
|
+
# Only when a bounded builtin read of project.json or config.json shows a
|
|
324
|
+
# `"tracker"` key does the hook make ONE fork — resolve-settings.cjs, the
|
|
325
|
+
# resolver the Git agent consumes, so the provider a session learns conventions
|
|
326
|
+
# for is the provider a Git spawn in the same project resolves (a personal
|
|
327
|
+
# `"tracker":"github"` silences a jira machine there, and a config.json git
|
|
328
|
+
# tracks is ignored, as the resolver ignores it). A `\u` escape (the only other
|
|
329
|
+
# JSON spelling of a letter) or a file the read cut short takes the same fork,
|
|
330
|
+
# so the parser — not a shell copy of it — decides every file it has to.
|
|
331
|
+
#
|
|
332
|
+
# [DR-10] What each path costs, and the whole reason for the shape above:
|
|
333
|
+
# - github, no project.json one stat, zero forks
|
|
334
|
+
# - github, project.json, no tracker one bounded read, zero forks
|
|
335
|
+
# - github, config.json of any shape not even stat-ed, zero forks
|
|
336
|
+
# - jira with learned conventions a stat, a builtin read, a stat, zero forks
|
|
337
|
+
# - project.json or (on a machine that names a provider) config.json shows a
|
|
338
|
+
# tracker one resolver fork, then the gates below
|
|
339
|
+
# The resolver's line is honoured only as far as its TRACKER token, and that
|
|
340
|
+
# token passes the same POSITIVE allowlist the sentinel's does. Every non-zero
|
|
341
|
+
# resolver exit, and every line that is not its shape, is the fail-closed github.
|
|
342
|
+
#
|
|
343
|
+
# Also gated on the project root being inside a git repository: the agent
|
|
344
|
+
# infers every repo-derived value from that history, and it writes the
|
|
345
|
+
# conventions file once and only once, so a session started outside a checkout
|
|
346
|
+
# would fix this provider's conventions at the unresolved sentinel for good.
|
|
347
|
+
#
|
|
348
|
+
# Not gated by the learning feature toggle: a user who turned learning off did
|
|
349
|
+
# not turn their issue tracker off.
|
|
350
|
+
TRACKER_SENTINEL="$TRACKER_DEVFLOW_DIR/.tracker.enabled"
|
|
351
|
+
TRACKER_PROJECT_FILE="$PROJECT_DEVFLOW_DIR/project.json"
|
|
352
|
+
TRACKER_PERSONAL_FILE="$PROJECT_DEVFLOW_DIR/config.json"
|
|
353
|
+
TRACKER_PROVIDER=""
|
|
354
|
+
if [ -f "$TRACKER_SENTINEL" ]; then
|
|
355
|
+
# `-n 16` bounds the bytes a hand-edited sentinel can pull into the shell; the
|
|
356
|
+
# read's status is not consulted (a file with no trailing newline returns
|
|
357
|
+
# non-zero having assigned the value) — only the allowlist below decides.
|
|
358
|
+
IFS= read -r -n 16 TRACKER_PROVIDER 2>/dev/null < "$TRACKER_SENTINEL"
|
|
359
|
+
fi
|
|
360
|
+
_SC_TRACKER_ASK="no"
|
|
361
|
+
# _sc_tracker_file_asks <file> — sets _SC_TRACKER_ASK=yes when a bounded builtin
|
|
362
|
+
# read of <file> shows a `"tracker"` key (or `\u`, the only other JSON spelling of
|
|
363
|
+
# a letter) or was cut short. `read -d ''` succeeds only when it stops SHORT of
|
|
364
|
+
# the end of the file — at a NUL byte or at 4097 characters, past the parser's
|
|
365
|
+
# 4096-byte bound — so a successful read is a file the shell cannot vouch for,
|
|
366
|
+
# and the parser decides.
|
|
367
|
+
_sc_tracker_file_asks() {
|
|
368
|
+
local _text=""
|
|
369
|
+
if IFS= read -r -d '' -n 4097 _text 2>/dev/null < "$1"; then
|
|
370
|
+
_SC_TRACKER_ASK="yes"
|
|
371
|
+
fi
|
|
372
|
+
case "$_text" in
|
|
373
|
+
*'"tracker"'*|*'\u'*) _SC_TRACKER_ASK="yes" ;;
|
|
374
|
+
esac
|
|
375
|
+
}
|
|
376
|
+
[ -f "$TRACKER_PROJECT_FILE" ] && _sc_tracker_file_asks "$TRACKER_PROJECT_FILE"
|
|
377
|
+
# The personal file can only narrow what the machine names, so it is read only
|
|
378
|
+
# when the sentinel named something and project.json has not already asked.
|
|
379
|
+
if [ -n "$TRACKER_PROVIDER" ] && [ "$_SC_TRACKER_ASK" = "no" ] && [ -f "$TRACKER_PERSONAL_FILE" ]; then
|
|
380
|
+
_sc_tracker_file_asks "$TRACKER_PERSONAL_FILE"
|
|
381
|
+
fi
|
|
382
|
+
if [ "$_SC_TRACKER_ASK" = "yes" ]; then
|
|
383
|
+
# The ONE fork. resolve-settings.cjs is a sibling of this hooks/ directory,
|
|
384
|
+
# installed with it; it makes only local git reads and no network call.
|
|
385
|
+
_SC_TRACKER_LINE=$(node "$SCRIPT_DIR/../resolve-settings.cjs" "$PROJECT_ROOT" 2>/dev/null) || _SC_TRACKER_LINE=""
|
|
386
|
+
case "$_SC_TRACKER_LINE" in
|
|
387
|
+
TRACKER=*" TRACKER_SOURCE="*)
|
|
388
|
+
_SC_TRACKER_LINE="${_SC_TRACKER_LINE#TRACKER=}"
|
|
389
|
+
TRACKER_PROVIDER="${_SC_TRACKER_LINE%% *}"
|
|
390
|
+
;;
|
|
391
|
+
*)
|
|
392
|
+
dbg "tracker settings line refused — failing closed to github"
|
|
393
|
+
TRACKER_PROVIDER=""
|
|
394
|
+
;;
|
|
395
|
+
esac
|
|
396
|
+
fi
|
|
397
|
+
# The provider, admitted by a POSITIVE allowlist BEFORE it reaches any path or
|
|
398
|
+
# any directive. Never `!= github`: the sentinel is user-writable and the
|
|
399
|
+
# resolver's line is a value this hook did not compose, so a negative test would
|
|
400
|
+
# admit every hostile string that merely is not the word "github" — quotes and
|
|
401
|
+
# newlines included — straight into a path test and into additionalContext.
|
|
402
|
+
# This admits exactly the two providers that have a background inference path.
|
|
403
|
+
# Reject, never repair — `jira-cloud` and `JIRA` are refused rather than
|
|
404
|
+
# normalised, so neither spawns an agent for a tracker the user did not name.
|
|
405
|
+
case "$TRACKER_PROVIDER" in
|
|
406
|
+
jira|linear) ;;
|
|
407
|
+
*) TRACKER_PROVIDER="" ;;
|
|
408
|
+
esac
|
|
409
|
+
TRACKER_CONVENTIONS="$TRACKER_DEVFLOW_DIR/tracker/$TRACKER_PROVIDER.md"
|
|
410
|
+
if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
411
|
+
TRACKER_ATTEMPTS_FILE="$TRACKER_DEVFLOW_DIR/.tracker.$TRACKER_PROVIDER.attempts"
|
|
412
|
+
TRACKER_CLAIM_FILE="$TRACKER_DEVFLOW_DIR/.tracker.processing"
|
|
413
|
+
# OD-14 — the attempt cap, per provider. A permanently broken tracker
|
|
414
|
+
# connection is routine; without a cap the hook respawns a background agent at
|
|
415
|
+
# every startup forever. One counter per provider, so a broken provider never
|
|
416
|
+
# spends another's attempts; the claim file stays global — one Tracker agent
|
|
417
|
+
# runs at a time on a machine, whichever provider it is learning.
|
|
418
|
+
TRACKER_ATTEMPTS_MAX=5
|
|
419
|
+
# Its OWN literal, deliberately NOT shared with Learning's 900 above: one
|
|
420
|
+
# shared constant would make a change to either feature silently reclassify the
|
|
421
|
+
# other's live runs as crashed. 600s is longer than the memory worker's 300s
|
|
422
|
+
# lock (a Tracker run does more — a capability probe plus bounded git scans)
|
|
423
|
+
# and shorter than Learning's 900s (no multi-part curation phase). Too long
|
|
424
|
+
# costs one session's delay before a crashed run retries; too short burns an
|
|
425
|
+
# attempt against the cap.
|
|
426
|
+
TRACKER_PROCESSING_STALE_SECS=600
|
|
427
|
+
|
|
428
|
+
# tracker_gates_open — every gate after the provider, cheapest first, each one
|
|
429
|
+
# an early `return 1`.
|
|
430
|
+
#
|
|
431
|
+
# A plain `sh` block has no early exit: a gate there can suppress only by
|
|
432
|
+
# clearing a flag that every later gate must then re-test, which turns the
|
|
433
|
+
# cheapest-first ordering that delivers [DR-10]'s zero-fork guarantee into a
|
|
434
|
+
# stack of re-entries rather than the linear sequence it is. A function supplies
|
|
435
|
+
# the early exit: each gate states its own condition once, and the order below
|
|
436
|
+
# IS the execution order, read top to bottom.
|
|
437
|
+
#
|
|
438
|
+
# TRACKER_ATTEMPTS is deliberately not `local`: it is this function's output,
|
|
439
|
+
# interpolated by the emitting block below.
|
|
440
|
+
tracker_gates_open() {
|
|
441
|
+
# Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
|
|
442
|
+
#
|
|
443
|
+
# The counter's shape is ONE decimal integer line and nothing else (PF-062 —
|
|
444
|
+
# document the shape of any file that gates an action, and keep absent and
|
|
445
|
+
# malformed distinct from a value). Absent means "no attempt yet" = 0.
|
|
446
|
+
# Malformed self-heals to 0 and is overwritten with a well-formed count on
|
|
447
|
+
# emission below, so a stray byte can never recur: refusing forever would
|
|
448
|
+
# disable inference permanently with no user-visible reason, and treating it as
|
|
449
|
+
# uncapped would defeat the cap. `read` returns non-zero at an EOF with no
|
|
450
|
+
# trailing newline but HAS assigned the variable, so its status is deliberately
|
|
451
|
+
# not consulted — only the value's shape is.
|
|
452
|
+
#
|
|
453
|
+
# `-n 16` bounds the BYTES consumed, which the `case` below cannot: the file is
|
|
454
|
+
# user-scope and hand-editable, and one very long line would otherwise be pulled
|
|
455
|
+
# whole into a shell variable and pattern-matched whole on the SessionStart
|
|
456
|
+
# critical path. Sixteen characters is past every arm's decision point — a
|
|
457
|
+
# well-formed count never reaches two digits, and the out-of-range arm fires at
|
|
458
|
+
# seven — so every arm keeps the verdict it would have reached unbounded.
|
|
459
|
+
# `2>/dev/null` is spelled BEFORE the input redirect for the same reason it is
|
|
460
|
+
# on the write below: redirections apply left to right, and a failed open is
|
|
461
|
+
# reported by the shell itself, so silencing stderr afterwards would be too late.
|
|
462
|
+
TRACKER_ATTEMPTS=""
|
|
463
|
+
if [ -f "$TRACKER_ATTEMPTS_FILE" ]; then
|
|
464
|
+
if [ -r "$TRACKER_ATTEMPTS_FILE" ]; then
|
|
465
|
+
IFS= read -r -n 16 TRACKER_ATTEMPTS 2>/dev/null < "$TRACKER_ATTEMPTS_FILE"
|
|
466
|
+
else
|
|
467
|
+
# Present but unreadable is NOT a fresh start. Leaving the variable empty
|
|
468
|
+
# would take the '' arm below and read as "no attempt yet", so an EACCES on
|
|
469
|
+
# the counter would emit at every startup forever — the unbounded retry the
|
|
470
|
+
# cap exists to prevent, triggered by exactly the broken-I/O condition that
|
|
471
|
+
# also stops the agent from ever writing its conventions file. Fails CLOSED; the
|
|
472
|
+
# re-arm path (devflow init / devflow tracker --set) owns recovery, as it
|
|
473
|
+
# does for any counter the hook leaves sitting at the cap.
|
|
474
|
+
dbg "tracker attempt counter unreadable — treated as at the cap"
|
|
475
|
+
TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
|
|
476
|
+
fi
|
|
477
|
+
fi
|
|
478
|
+
case "$TRACKER_ATTEMPTS" in
|
|
479
|
+
'') TRACKER_ATTEMPTS=0 ;;
|
|
480
|
+
*[!0-9]*)
|
|
481
|
+
dbg "tracker attempt counter malformed — self-healed to 0"
|
|
482
|
+
TRACKER_ATTEMPTS=0
|
|
483
|
+
;;
|
|
484
|
+
0) ;; # a bare zero is a well-formed count, not a padded one
|
|
485
|
+
0*)
|
|
486
|
+
# A zero-padded count is ONE string that this hook's two consumers of it read
|
|
487
|
+
# in DIFFERENT BASES. `[ "$N" -ge "$MAX" ]` parses base 10, so `08` compares
|
|
488
|
+
# as eight; the `$(( N + 1 ))` that writes the next count is shell arithmetic,
|
|
489
|
+
# where a leading `0` means OCTAL and `08` is "value too great for base" — an
|
|
490
|
+
# error that escapes the write's own `2>/dev/null`, because expansion runs
|
|
491
|
+
# before redirection. Nothing in the padded shape says which reading was
|
|
492
|
+
# meant, so it self-heals to 0 with every other malformed value instead of
|
|
493
|
+
# being carried into the disagreement. This hook writes a bare decimal, so
|
|
494
|
+
# padding came from elsewhere. Placed BEFORE the digit-count arm so a
|
|
495
|
+
# six-character `000008` heals rather than reading as "six digits, at the cap".
|
|
496
|
+
dbg "tracker attempt counter zero-padded — self-healed to 0"
|
|
497
|
+
TRACKER_ATTEMPTS=0
|
|
498
|
+
;;
|
|
499
|
+
???????*)
|
|
500
|
+
# Bounded before the comparison: `[ "$N" -ge 5 ]` on a value past intmax_t
|
|
501
|
+
# prints "integer expression expected" to stderr and takes the FALSE branch,
|
|
502
|
+
# so an unbounded digit string would fail OPEN — uncapped — and leak a shell
|
|
503
|
+
# error. `-n 16` above already puts that state out of reach; this arm is the
|
|
504
|
+
# semantic half and survives a change to the read bound. It matches SEVEN
|
|
505
|
+
# digits or more (the earlier arms have taken every non-digit and every
|
|
506
|
+
# zero-padded value), which is the shape CLAUDE.md documents as "at the cap",
|
|
507
|
+
# and a count that matters needs one digit, so anything reaching here is past
|
|
508
|
+
# the cap by six orders of magnitude. Six digits and fewer are compared as
|
|
509
|
+
# the integers they are.
|
|
510
|
+
dbg "tracker attempt counter out of range — treated as at the cap"
|
|
511
|
+
TRACKER_ATTEMPTS="$TRACKER_ATTEMPTS_MAX"
|
|
512
|
+
;;
|
|
513
|
+
esac
|
|
514
|
+
if [ "$TRACKER_ATTEMPTS" -ge "$TRACKER_ATTEMPTS_MAX" ]; then
|
|
515
|
+
dbg "tracker directive suppressed: attempt cap reached ($TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
|
|
516
|
+
return 1
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
# Gate 2 — the repository. Every repo-derived value the Tracker agent writes
|
|
520
|
+
# comes from the project root's history, and the agent refuses to infer from
|
|
521
|
+
# history at all when the root carries no git marker — outside a checkout it
|
|
522
|
+
# would write its unresolved sentinel into every one of those sections. It
|
|
523
|
+
# writes the conventions file ONCE, create-exclusive, so that degraded file
|
|
524
|
+
# would be this machine's conventions for good, and the attempt that produced
|
|
525
|
+
# it would be spent against the cap.
|
|
526
|
+
#
|
|
527
|
+
# The helper was sourced once, at the top, where the project gate
|
|
528
|
+
# (D-HOOKS-GIT-ONLY) needs it too; this gate adds no read of its own. Its
|
|
529
|
+
# bounded walk of `-e` tests answers the question with no subprocess, which is
|
|
530
|
+
# what keeps this gate ahead of the forking gates below. A helper that
|
|
531
|
+
# failed to source is a command that is not found, which takes the suppressing
|
|
532
|
+
# branch — fail closed, and silently, since the redirect covers the lookup
|
|
533
|
+
# failure too. Deliberately the marker alone, not df_is_project_root: the
|
|
534
|
+
# tracker conventions are machine-wide, and HOME-as-a-repository only rules out
|
|
535
|
+
# PROJECT data.
|
|
536
|
+
if ! df_has_git_marker "$PROJECT_ROOT" 2>/dev/null; then
|
|
537
|
+
dbg "tracker directive suppressed: project root is not a git repository"
|
|
538
|
+
return 1
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
# Gate 3 — source. Only a fresh session (startup) and a cleared one (clear)
|
|
542
|
+
# begin work that needs conventions; resume and compact continue a session that
|
|
543
|
+
# already had its chance, so re-asking there would spawn an agent mid-flight.
|
|
544
|
+
TRACKER_SOURCE=$(printf '%s' "$INPUT" | json_field "source" "")
|
|
545
|
+
case "$TRACKER_SOURCE" in
|
|
546
|
+
startup|clear) ;;
|
|
547
|
+
*)
|
|
548
|
+
dbg "tracker directive suppressed: not a session start"
|
|
549
|
+
return 1
|
|
550
|
+
;;
|
|
551
|
+
esac
|
|
552
|
+
|
|
553
|
+
# Gate 4 — the claim file, mirroring Section 2's freshness check. A FRESH
|
|
554
|
+
# claim means a live Tracker agent owns the run. A STALE one means a previous
|
|
555
|
+
# run crashed, so re-arm — but never delete it: re-claiming is the agent's job
|
|
556
|
+
# (it touches the file), and a hook that deleted it would race a slow-but-live
|
|
557
|
+
# run. An unreadable mtime falls to the suppressing branch (fail closed).
|
|
558
|
+
if [ -f "$TRACKER_CLAIM_FILE" ]; then
|
|
559
|
+
source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
|
|
560
|
+
_SC_TRACKER_MTIME=$(get_mtime "$TRACKER_CLAIM_FILE" 2>/dev/null || true)
|
|
561
|
+
_SC_TRACKER_NOW=$(date +%s)
|
|
562
|
+
if [ -n "$_SC_TRACKER_MTIME" ] && [ $(( _SC_TRACKER_NOW - _SC_TRACKER_MTIME )) -ge "$TRACKER_PROCESSING_STALE_SECS" ]; then
|
|
563
|
+
dbg "tracker claim file is stale — previous run crashed, re-arming"
|
|
564
|
+
else
|
|
565
|
+
dbg "tracker directive suppressed: fresh .tracker.processing (live agent owns the run)"
|
|
566
|
+
return 1
|
|
567
|
+
fi
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
# Gate 5 — the shape of the two PATHS the directive carries alongside the two
|
|
571
|
+
# allowlisted tokens. Decided once at the top of the file, where both values are
|
|
572
|
+
# resolved; consulted here because this is one of the two sinks that interpolate
|
|
573
|
+
# them, and a control stated once for the file is not a control at a sink that
|
|
574
|
+
# never consults it (PF-023). The third path, the conventions file, is composed
|
|
575
|
+
# from the gated devflow directory and the allowlisted provider alone, so this
|
|
576
|
+
# gate covers it too.
|
|
577
|
+
if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
|
|
578
|
+
dbg "tracker directive suppressed: interpolated path shape rejected"
|
|
579
|
+
return 1
|
|
580
|
+
fi
|
|
581
|
+
|
|
582
|
+
# Gate 6 — the increment must LAND. [DR-02] Increment on EMISSION, not on the
|
|
583
|
+
# agent's completion: a crashed agent never reaches its own increment, so without
|
|
584
|
+
# this the crash-loop case stays uncapped even with the agent-side counter. The
|
|
585
|
+
# agent DELETES the counter on a successful write, so a healthy path never
|
|
586
|
+
# accumulates. printf is a builtin and the redirect is the shell's — no fork.
|
|
587
|
+
# `2>/dev/null` is spelled FIRST: redirections are applied left to right, and a
|
|
588
|
+
# failed open on the counter path is reported by the shell itself, so silencing
|
|
589
|
+
# stderr after the failing redirect would be too late to keep the hook quiet.
|
|
590
|
+
#
|
|
591
|
+
# Emission is CONDITIONAL on the write: an increment that cannot persist is a cap
|
|
592
|
+
# that can never engage, and the very same unwritable ~/.devflow also stops the
|
|
593
|
+
# agent writing its conventions file while leaving the sentinel in place.
|
|
594
|
+
# Emitting anyway would spawn a background agent at every startup, forever — an
|
|
595
|
+
# unbounded retry driven by the one condition the cap exists to bound.
|
|
596
|
+
if ! printf '%s\n' "$(( TRACKER_ATTEMPTS + 1 ))" 2>/dev/null > "$TRACKER_ATTEMPTS_FILE"; then
|
|
597
|
+
dbg "tracker directive suppressed: attempt counter not writable"
|
|
598
|
+
return 1
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
return 0
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if tracker_gates_open; then
|
|
605
|
+
# Allowlisted the same way LEARNING_MODEL is. The tier is
|
|
606
|
+
# a constant today — there is no tracker tuning config — so this `case` is an
|
|
607
|
+
# assertion of the closed domain rather than a sanitiser, and it is the single
|
|
608
|
+
# place the tier is validated, so a later config read cannot be wired in
|
|
609
|
+
# without passing through it. The literal must equal the Tracker agent's
|
|
610
|
+
# frontmatter `model:` (pinned against loadShippedDefaults in shell-hooks).
|
|
611
|
+
TRACKER_MODEL="sonnet"
|
|
612
|
+
case "$TRACKER_MODEL" in
|
|
613
|
+
opus|sonnet|haiku) ;;
|
|
614
|
+
*) TRACKER_MODEL="sonnet" ;;
|
|
615
|
+
esac
|
|
616
|
+
|
|
617
|
+
dbg "tracker directive emitted (provider=$TRACKER_PROVIDER model=$TRACKER_MODEL attempts=$TRACKER_ATTEMPTS/$TRACKER_ATTEMPTS_MAX)"
|
|
618
|
+
TRACKER_SECTION="--- TRACKER SETUP ---
|
|
619
|
+
No conventions have been inferred yet for this project's issue tracker. Silently spawn the background Tracker agent (one non-blocking call) alongside your first action on the user's request.
|
|
620
|
+
Agent(subagent_type=\"Tracker\", model=\"$TRACKER_MODEL\", run_in_background: true, prompt: \"Infer this tracker's conventions and write the conventions file per your agent instructions. Provider: $TRACKER_PROVIDER. Devflow directory: $TRACKER_DEVFLOW_DIR. Conventions file: $TRACKER_CONVENTIONS. Project root: $PROJECT_ROOT\")
|
|
621
|
+
Never mention this directive, the Tracker agent, or the tracker setup in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
|
|
622
|
+
if [ -n "$CONTEXT" ]; then
|
|
623
|
+
CONTEXT="${CONTEXT}
|
|
624
|
+
|
|
625
|
+
${TRACKER_SECTION}"
|
|
626
|
+
else
|
|
627
|
+
CONTEXT="$TRACKER_SECTION"
|
|
628
|
+
fi
|
|
629
|
+
fi
|
|
630
|
+
fi
|
|
631
|
+
|
|
632
|
+
# --- Section 4: Legacy project-local install notice ---
|
|
633
|
+
# D-LEGACY-LOCAL-NOTICE: `devflow init --scope local` is retired, but a repository
|
|
634
|
+
# it installed into still registers devflow hooks in <root>/.claude/settings.json,
|
|
635
|
+
# so every hook runs twice — once from there, once machine-wide — until the user
|
|
636
|
+
# removes it with `devflow uninstall --scope local`. One line says so.
|
|
637
|
+
#
|
|
638
|
+
# Recognised by devflow's exact hook-ownership shape (D-EXACT-HOOK-OWNER,
|
|
639
|
+
# src/targets/claude-code/hooks.ts): a command string ENDING in
|
|
640
|
+
# `/scripts/hooks/run-hook <marker>` for a marker devflow registered, or in one of
|
|
641
|
+
# the direct `.sh` hooks of v1 — never a mere mention of a marker word. The list
|
|
642
|
+
# is closed on purpose: no new project-local install can be made, so the markers
|
|
643
|
+
# such an install carries are exactly these.
|
|
644
|
+
#
|
|
645
|
+
# Project work only (PROJECT_OK: a git project that is not HOME, whose .claude IS
|
|
646
|
+
# the machine-wide one), and only for a session that starts fresh. Cost: one stat
|
|
647
|
+
# on every session; a bounded builtin read only when that settings.json exists;
|
|
648
|
+
# forks only when it matches.
|
|
649
|
+
LEGACY_SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.json"
|
|
650
|
+
if [ -n "$PROJECT_OK" ] && [ -f "$LEGACY_SETTINGS_FILE" ]; then
|
|
651
|
+
_SC_LEGACY_TEXT=""
|
|
652
|
+
IFS= read -r -d '' -n 65536 _SC_LEGACY_TEXT 2>/dev/null < "$LEGACY_SETTINGS_FILE"
|
|
653
|
+
_SC_LEGACY_MARKERS='capture-prompt|capture-turn|capture-question|session-start-context|session-start-memory|pre-compact-memory|memory-worker|preamble|session-start-orchestrator|session-start-classification|ambient-prompt|ensure-proxy|spawn-dream-worker|prompt-capture-memory|stop-update-memory|stop-update-learning|session-end-learning|session-end-decisions|session-end-knowledge-refresh|sidecar-dispatch|sidecar-capture|sidecar-evaluate|dream-dispatch|dream-capture|dream-evaluate'
|
|
654
|
+
_SC_LEGACY_RE="/scripts/hooks/(run-hook ($_SC_LEGACY_MARKERS)|(stop-update-memory|session-start-memory|pre-compact-memory|ambient-prompt)\\.sh)[[:space:]]*\""
|
|
655
|
+
if [[ $_SC_LEGACY_TEXT =~ $_SC_LEGACY_RE ]]; then
|
|
656
|
+
# The machine-wide Claude Code directory is never "legacy", even when a repo
|
|
657
|
+
# holds it ($CLAUDE_CONFIG_DIR, honoured only when absolute).
|
|
658
|
+
_SC_MACHINE_CLAUDE="$HOME/.claude"
|
|
659
|
+
case "${CLAUDE_CONFIG_DIR:-}" in /*) _SC_MACHINE_CLAUDE="$CLAUDE_CONFIG_DIR" ;; esac
|
|
660
|
+
_SC_LEGACY_REAL=$(cd -P "$PROJECT_ROOT/.claude" 2>/dev/null && pwd -P) || _SC_LEGACY_REAL=""
|
|
661
|
+
_SC_MACHINE_REAL=$(cd -P "$_SC_MACHINE_CLAUDE" 2>/dev/null && pwd -P) || _SC_MACHINE_REAL=""
|
|
662
|
+
_SC_LEGACY_SOURCE=$(printf '%s' "$INPUT" | json_field "source" "")
|
|
663
|
+
if [ -n "$_SC_LEGACY_REAL" ] && [ "$_SC_LEGACY_REAL" != "$_SC_MACHINE_REAL" ]; then
|
|
664
|
+
case "$_SC_LEGACY_SOURCE" in
|
|
665
|
+
startup|clear)
|
|
666
|
+
dbg "legacy project-local install notice emitted"
|
|
667
|
+
LEGACY_SECTION='Devflow notice: this repository still holds a retired project-local devflow install (its .claude/settings.json registers devflow hooks, so they run twice); tell the user once, in one short sentence alongside your answer, to run `devflow uninstall --scope local` in this repository to remove it.'
|
|
668
|
+
if [ -n "$CONTEXT" ]; then
|
|
669
|
+
CONTEXT="${CONTEXT}
|
|
670
|
+
|
|
671
|
+
${LEGACY_SECTION}"
|
|
672
|
+
else
|
|
673
|
+
CONTEXT="$LEGACY_SECTION"
|
|
674
|
+
fi
|
|
675
|
+
;;
|
|
676
|
+
esac
|
|
677
|
+
fi
|
|
678
|
+
fi
|
|
679
|
+
fi
|
|
680
|
+
|
|
167
681
|
# --- Output ---
|
|
168
682
|
|
|
169
683
|
# Only output if we have something to inject
|