devflow-kit 3.0.0 → 3.1.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 +22 -0
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +2 -2
- package/dist/cli/agents-view/state.js +2 -2
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +7 -6
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +34 -32
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +267 -129
- package/dist/cli/commands/memory.js +1 -1
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +20 -20
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/dynamic-build.md +18 -4
- package/dist/commands/dynamic-plan.md +19 -5
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/release.md +15 -1
- package/dist/commands/research.md +1 -1
- package/dist/commands/resolve.md +8 -9
- package/dist/core/agent-frontmatter.js +3 -3
- package/dist/core/agent-models.js +6 -6
- package/dist/core/agent-state.js +2 -2
- package/dist/core/ansi.js +2 -2
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +3 -3
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +25 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -80
- package/dist/core/learning-store.js +61 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +4 -5
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +2 -16
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +24 -24
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +25 -11
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +1 -4
- package/src/assets/agents/design.md +2 -2
- package/src/assets/agents/diagnose.md +1 -1
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +1 -1
- package/src/assets/agents/review.md +3 -3
- package/src/assets/agents/scrutinize.md +1 -1
- package/src/assets/agents/skim.md +1 -1
- package/src/assets/agents/triage.md +9 -9
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +1 -1
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/dynamic-build.mds +1 -1
- package/src/assets/commands/dynamic-plan.mds +3 -3
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/release.md +15 -1
- package/src/assets/commands/research.mds +1 -1
- package/src/assets/commands/resolve.mds +8 -9
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +1 -1
- package/src/assets/mds/tracker/_jira.mds +1 -1
- package/src/assets/mds/tracker/_linear.mds +1 -1
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -0
- package/src/assets/scripts/hooks/background-memory-update +28 -22
- package/src/assets/scripts/hooks/capture-turn +1 -17
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +1 -1
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +348 -814
- package/src/assets/scripts/hooks/json-parse +3 -2
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3102 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/queue-append +2 -2
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +40 -18
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/targets/claude-code/templates/managed-settings.json +3 -3
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -56,10 +56,9 @@ _df_nongit_root() {
|
|
|
56
56
|
# DF_LEDGER_ROOT — where the learning ledger and its queue live.
|
|
57
57
|
#
|
|
58
58
|
# D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
|
|
59
|
-
# --worktree`) has its own toplevel, so a ledger anchored there restarts
|
|
60
|
-
#
|
|
61
|
-
# with main's
|
|
62
|
-
# for good). The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
|
|
59
|
+
# --worktree`) has its own toplevel, so a ledger anchored there restarts its
|
|
60
|
+
# numbering at 001 — main's decisions are invisible in it, and the IDs it mints collide
|
|
61
|
+
# with main's. The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
|
|
63
62
|
# is the main worktree — the parent of `--git-common-dir` — when that common dir
|
|
64
63
|
# is a `<dir>/.git` directory AND `<dir>/.devflow` already exists; otherwise it is
|
|
65
64
|
# DF_ROOT. The existence test keeps a main checkout that never ran devflow from
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
|
|
9
9
|
# the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
|
|
10
10
|
#
|
|
11
|
-
# Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines)
|
|
11
|
+
# Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines),
|
|
12
|
+
# then the path of the decisions index when it lists entries.
|
|
12
13
|
# Section 2: Learning maintenance directive — when captured turns are pending in
|
|
13
14
|
# the learning queue (or a crashed run left a stale .processing batch), instructs
|
|
14
15
|
# the main model to spawn the background Learning agent with the resolved model.
|
|
@@ -85,7 +86,7 @@ fi
|
|
|
85
86
|
# Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
|
|
86
87
|
# git-project session regardless of feature toggles, so memory-off projects
|
|
87
88
|
# (learning/knowledge only) still get .devflow/ ignored — this is the
|
|
88
|
-
# memory-independent path that fixes the gitignore/memory coupling
|
|
89
|
+
# memory-independent path that fixes the gitignore/memory coupling. Single
|
|
89
90
|
# source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
|
|
90
91
|
# block context injection. Marker keeps it O(1).
|
|
91
92
|
[ -n "$PROJECT_OK" ] && [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ] && source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
|
|
@@ -101,7 +102,8 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
|
|
|
101
102
|
#
|
|
102
103
|
# Section 2 embeds $LEDGER_ROOT; Section 3 embeds $PROJECT_ROOT and
|
|
103
104
|
# $TRACKER_DEVFLOW_DIR — each inside a double-quoted `prompt: "..."` string the
|
|
104
|
-
# model reads out of additionalContext.
|
|
105
|
+
# model reads out of additionalContext. Section 1's index line embeds
|
|
106
|
+
# $LEDGER_ROOT too, as the path the model reads the decisions index from. The provider and model TOKENS in those
|
|
105
107
|
# directives are admitted by positive allowlists, and so are these: the gate is the
|
|
106
108
|
# POSITIVE shape ^[A-Za-z0-9/._+-]+$, expressed as "rejects if any character falls
|
|
107
109
|
# outside it".
|
|
@@ -134,16 +136,16 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
|
|
|
134
136
|
# admitted set is locale-dependent, and no rule here rests on it.
|
|
135
137
|
#
|
|
136
138
|
# 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 (
|
|
139
|
+
# interpolates them, so no sink can embed a value no gate saw (the
|
|
138
140
|
# invariant belongs at the convergence point all callers pass through, not in
|
|
139
141
|
# whichever section someone remembered). `case` is a shell builtin, so the GitHub
|
|
140
142
|
# path still forks zero times [DR-10]. The empty arm is explicit: an unset root
|
|
141
143
|
# must not read as "no forbidden character, therefore safe".
|
|
142
144
|
#
|
|
143
145
|
# ONE gate per VALUE, applied to the root each section embeds, not one over their
|
|
144
|
-
# concatenation.
|
|
145
|
-
# root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
|
|
146
|
-
# for
|
|
146
|
+
# concatenation. Sections 1 and 2 interpolate $LEDGER_ROOT alone — the main
|
|
147
|
+
# worktree's root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
|
|
148
|
+
# checked for them, not the checkout's own; only Section 3 interpolates
|
|
147
149
|
# $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
|
|
148
150
|
# ~/.devflow shape suppress the Learning directive as well — a value Section 2
|
|
149
151
|
# never embeds, silently disabling the whole learning pipeline for any machine
|
|
@@ -211,10 +213,27 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
211
213
|
fi
|
|
212
214
|
fi
|
|
213
215
|
done
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
# The index line names the decisions index, so the main model can pass it on
|
|
217
|
+
# as DECISIONS_CONTEXT. It interpolates $LEDGER_ROOT, so it needs that root's
|
|
218
|
+
# shape gate, and it is left out when the index lists no entry: an empty
|
|
219
|
+
# corpus renders `(none)`. Builtins only — no fork on any path.
|
|
220
|
+
_SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
|
|
221
|
+
if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && [ -s "$_SC_INDEX" ]; then
|
|
222
|
+
_SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
|
|
223
|
+
[ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
|
|
224
|
+
fi
|
|
225
|
+
if [ -n "$DECISIONS_TLDR" ] || [ -n "$DECISIONS_INDEX_LINE" ]; then
|
|
226
|
+
dbg "Decisions TL;DR or index found"
|
|
227
|
+
DECISIONS_SECTION="--- PROJECT DECISIONS (TL;DR) ---"
|
|
228
|
+
if [ -n "$DECISIONS_TLDR" ]; then
|
|
229
|
+
DECISIONS_SECTION="${DECISIONS_SECTION}
|
|
217
230
|
$(printf '%b' "$DECISIONS_TLDR")"
|
|
231
|
+
fi
|
|
232
|
+
# The index line goes last.
|
|
233
|
+
if [ -n "$DECISIONS_INDEX_LINE" ]; then
|
|
234
|
+
DECISIONS_SECTION="${DECISIONS_SECTION}
|
|
235
|
+
${DECISIONS_INDEX_LINE}"
|
|
236
|
+
fi
|
|
218
237
|
if [ -n "$CONTEXT" ]; then
|
|
219
238
|
CONTEXT="${CONTEXT}
|
|
220
239
|
|
|
@@ -227,11 +246,14 @@ ${DECISIONS_SECTION}"
|
|
|
227
246
|
fi
|
|
228
247
|
|
|
229
248
|
# --- Section 2: Learning maintenance directive ---
|
|
230
|
-
# Emitted when captured turns are waiting: queue non-empty, or a
|
|
231
|
-
# .processing
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
249
|
+
# Emitted when captured turns are waiting: the queue is non-empty, or a
|
|
250
|
+
# .processing claim has had no heartbeat for PROCESSING_STALE_SECS and its batch
|
|
251
|
+
# needs a new owner. A FRESH .processing means a live Learning agent holds the
|
|
252
|
+
# claim, so the directive is suppressed even if new turns queued since. This
|
|
253
|
+
# check is advisory and decides only whether to spawn: the claim itself is taken
|
|
254
|
+
# by json-helper's claim-queue op under the learning lock, which applies the same
|
|
255
|
+
# threshold (D-OWNED-CLAIM, lib/learning-store.cjs), so a spawn that loses a race
|
|
256
|
+
# exits on `busy`.
|
|
235
257
|
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
236
258
|
QUEUE_FILE="$LEARNING_DIR/.pending-turns.jsonl"
|
|
237
259
|
PROCESSING_FILE="$LEARNING_DIR/.pending-turns.processing"
|
|
@@ -440,8 +462,8 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
440
462
|
tracker_gates_open() {
|
|
441
463
|
# Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
|
|
442
464
|
#
|
|
443
|
-
# The counter's shape is ONE decimal integer line and nothing else
|
|
444
|
-
# document the shape of any file that gates an action, and keep absent and
|
|
465
|
+
# The counter's shape is ONE decimal integer line and nothing else
|
|
466
|
+
# (document the shape of any file that gates an action, and keep absent and
|
|
445
467
|
# malformed distinct from a value). Absent means "no attempt yet" = 0.
|
|
446
468
|
# Malformed self-heals to 0 and is overwritten with a well-formed count on
|
|
447
469
|
# emission below, so a stray byte can never recur: refusing forever would
|
|
@@ -571,7 +593,7 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
571
593
|
# allowlisted tokens. Decided once at the top of the file, where both values are
|
|
572
594
|
# resolved; consulted here because this is one of the two sinks that interpolate
|
|
573
595
|
# them, and a control stated once for the file is not a control at a sink that
|
|
574
|
-
# never consults it
|
|
596
|
+
# never consults it. The third path, the conventions file, is composed
|
|
575
597
|
# from the gated devflow directory and the allowlisted provider alone, so this
|
|
576
598
|
# gate covers it too.
|
|
577
599
|
if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The ONE parser of devflow's per-repository config files — the team-committed
|
|
4
4
|
// `.devflow/project.json` and the personal, uncommitted `.devflow/config.json`
|
|
5
|
-
// (
|
|
5
|
+
// (the invariant lives at the sink every reader passes through).
|
|
6
6
|
// Installed beside its two callers as ~/.devflow/scripts/lib/project-config.cjs:
|
|
7
7
|
// resolve-evidence-policy.cjs reads `evidence` and `compliance` at every source
|
|
8
8
|
// resolve-settings.cjs reads every key of both files, locally
|
|
9
9
|
//
|
|
10
10
|
// Pure except the two readers (readBoundedRegularFile, readMachineManifest), which
|
|
11
11
|
// only ever read. Nothing here writes, spawns or prints: devflow never writes
|
|
12
|
-
// project.json
|
|
12
|
+
// project.json, which the team owns and commits.
|
|
13
13
|
//
|
|
14
14
|
// D-PROJECT-CONFIG: `.devflow/project.json` is a JSON object whose every key is
|
|
15
15
|
// optional and whose unknown keys are ignored:
|
|
@@ -898,7 +898,7 @@ function needsDiff(x) {
|
|
|
898
898
|
/**
|
|
899
899
|
* D-LADDER: the arms, in the order they are tried. PRECEDENCE is derived from
|
|
900
900
|
* this table. Every arm but the last is a POSITIVE match; the last is the
|
|
901
|
-
* conservative default (
|
|
901
|
+
* conservative default (a verified state is only ever reached by
|
|
902
902
|
* a positive conjunction, never because nothing else matched).
|
|
903
903
|
*
|
|
904
904
|
* @type {readonly Arm[]}
|
|
@@ -978,7 +978,7 @@ const ARMS = Object.freeze([
|
|
|
978
978
|
/**
|
|
979
979
|
* D-LADDER: the order `classify` tries its arms in — first match wins, and the
|
|
980
980
|
* terminal arm is the conservative UNVERIFIED, so no state is ever reached by
|
|
981
|
-
* exhaustion
|
|
981
|
+
* exhaustion. DERIVED from ARMS, so the order the contract states
|
|
982
982
|
* (parity-pinned to this list) and the order the code runs cannot drift apart.
|
|
983
983
|
*/
|
|
984
984
|
const PRECEDENCE = Object.freeze(/** @type {State[]} */ (ARMS.map(arm => arm.state)));
|
|
@@ -1446,7 +1446,7 @@ function parseEvidenceComment(text) {
|
|
|
1446
1446
|
}
|
|
1447
1447
|
|
|
1448
1448
|
// ---------------------------------------------------------------------------
|
|
1449
|
-
// splice — only the bytes between the markers are devflow's
|
|
1449
|
+
// splice — only the bytes between the markers are devflow's
|
|
1450
1450
|
// ---------------------------------------------------------------------------
|
|
1451
1451
|
|
|
1452
1452
|
/**
|
|
@@ -40,19 +40,19 @@
|
|
|
40
40
|
// posted" from "the script broke": the first is final, the second is retried.
|
|
41
41
|
//
|
|
42
42
|
// Design constraints (binding):
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
43
|
+
// - the file sink — the one file this script writes — goes via
|
|
44
|
+
// temp-sibling + rename (atomic same-fs write; readers see old-or-new,
|
|
45
|
+
// never a momentarily absent file). `--emit` prints to stdout and
|
|
46
|
+
// touches no file, so it has nothing to protect
|
|
47
|
+
// - never call process.exit() inside any scope with pending cleanup or
|
|
48
|
+
// buffered output; main() returns an exit code; the single top-level
|
|
49
|
+
// boundary writes stdout SYNCHRONOUSLY then sets process.exitCode so
|
|
50
|
+
// nothing is truncated and no finally block is skipped
|
|
51
|
+
// - all regexes are bounded (no unbounded [\s\S]*); skip-list checked
|
|
52
|
+
// before any replacement; the unterminated-header pattern uses a
|
|
53
|
+
// character class instead of a lazy quantifier for bounded scan
|
|
54
|
+
// - self-contained sink-side control — never assumes upstream masking
|
|
55
|
+
// happened; the scrubber is authoritative for its own rule set
|
|
56
56
|
|
|
57
57
|
'use strict';
|
|
58
58
|
|
|
@@ -98,7 +98,7 @@ const NONCE_HEX_CHARS = 32;
|
|
|
98
98
|
* rule then describes a property of every body this script can emit, instead of
|
|
99
99
|
* an obligation nine documents have to restate correctly.
|
|
100
100
|
*
|
|
101
|
-
*
|
|
101
|
+
* Bounded — a fixed alternation over two literals, anchored per line by
|
|
102
102
|
* the `m` flag, with no quantifier to backtrack through. The trailing space is
|
|
103
103
|
* load-bearing: it is what keeps prose such as `D11-FAILURE` out of the refusal.
|
|
104
104
|
*/
|
|
@@ -268,7 +268,7 @@ function hasMixedAlphanumerics(s) {
|
|
|
268
268
|
// Scrubbing pass
|
|
269
269
|
//
|
|
270
270
|
// Rules are applied in the declared order. Each rule uses a bounded regex to
|
|
271
|
-
// avoid catastrophic backtracking
|
|
271
|
+
// avoid catastrophic backtracking. The final secret-assignment rule
|
|
272
272
|
// operates line-by-line and applies entropy + character-class heuristics to
|
|
273
273
|
// avoid false positives on config references.
|
|
274
274
|
// ---------------------------------------------------------------------------
|
|
@@ -375,7 +375,7 @@ function scrub(content) {
|
|
|
375
375
|
// (`this.apiToken`, `api-key`). Without these the rule only ever fired on a
|
|
376
376
|
// bare column-0 assignment, which is the rarest form inside a review finding.
|
|
377
377
|
// The declarator group is bounded {0,3} and each iteration must consume a
|
|
378
|
-
// literal keyword, so the added alternation cannot backtrack
|
|
378
|
+
// literal keyword, so the added alternation cannot backtrack.
|
|
379
379
|
//
|
|
380
380
|
// Conditions for replacement (all must hold):
|
|
381
381
|
// (a) The KEY matches SECRET_KEY_RE — not merely the line, so a prose line
|
|
@@ -590,7 +590,7 @@ function frameEmit(scrubbed, scrubLine, nonceSource) {
|
|
|
590
590
|
// ---------------------------------------------------------------------------
|
|
591
591
|
// main — returns an exit code (never calls process.exit() internally)
|
|
592
592
|
//
|
|
593
|
-
//
|
|
593
|
+
// No process.exit() inside any scope that has pending cleanup or
|
|
594
594
|
// buffered output. main() returns a numeric code for error paths or an
|
|
595
595
|
// object {scrubLine} for the success path. The single top-level boundary
|
|
596
596
|
// writes stdout synchronously and sets process.exitCode — nothing is
|
|
@@ -656,7 +656,7 @@ function readInput(inputPath) {
|
|
|
656
656
|
function runFileMode(args, content) {
|
|
657
657
|
const { result, counts } = scrub(content);
|
|
658
658
|
|
|
659
|
-
// ---- atomic write (
|
|
659
|
+
// ---- atomic write (temp-sibling + rename) ----
|
|
660
660
|
const tmpPath = args.outputPath + '.tmp';
|
|
661
661
|
try {
|
|
662
662
|
fs.writeFileSync(tmpPath, result, 'utf8');
|
|
@@ -686,7 +686,7 @@ function runFileMode(args, content) {
|
|
|
686
686
|
* prints them, so there is nothing for a write discipline to protect: a scrubbed
|
|
687
687
|
* comment body put on disk is a second copy with the input directory's lifetime,
|
|
688
688
|
* and proving that directory writable would let a filesystem property refuse a
|
|
689
|
-
* clean, fully gated body. The file mode's temp-sibling + rename
|
|
689
|
+
* clean, fully gated body. The file mode's temp-sibling + rename guards
|
|
690
690
|
* the one file this script does write.
|
|
691
691
|
*
|
|
692
692
|
* @param {string} content
|
|
@@ -762,7 +762,7 @@ function main(argv, deps) {
|
|
|
762
762
|
//
|
|
763
763
|
// This is the ONLY place that writes to stdout and sets process.exitCode.
|
|
764
764
|
// No other code path may call process.exit() or write to stdout.
|
|
765
|
-
// (
|
|
765
|
+
// (single synchronous write, no pending cleanup, no buffered output)
|
|
766
766
|
//
|
|
767
767
|
// AMENDED for --emit, not bypassed. main()'s return widened from
|
|
768
768
|
// `number | {scrubLine}` to also carry `{emitLine, body, code}`, and the write
|
|
@@ -445,7 +445,7 @@ function isChangelogOnly(paths) {
|
|
|
445
445
|
/**
|
|
446
446
|
* D-TRACE-CLASSIFY: one first-parent commit's class. First match wins, in
|
|
447
447
|
* CLASSES order, and the terminal arm is `untraced` — an input no rule
|
|
448
|
-
* recognises surfaces in the confirm rather than passing silently
|
|
448
|
+
* recognises surfaces in the confirm rather than passing silently.
|
|
449
449
|
*
|
|
450
450
|
* @param {Commit} commit
|
|
451
451
|
* @param {ClassifyContext} ctx
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The policy is plumbing, decided once by the caller: this script prints it plus
|
|
12
12
|
// the mechanism inputs, and operations only ever see the inputs. It WRITES NOTHING
|
|
13
13
|
// — no file, no git ref, no remote state — so `.devflow/project.json` stays a
|
|
14
|
-
// team-owned file that only the team commits
|
|
14
|
+
// team-owned file that only the team commits. A committed
|
|
15
15
|
// `.devflow/policy.json` is detected by presence and never parsed
|
|
16
16
|
// (D-POLICY-JSON-RETIRED).
|
|
17
17
|
//
|
|
@@ -432,7 +432,7 @@ function readManifestCompliance() {
|
|
|
432
432
|
* starting (ENOENT), timing out, overflowing its buffer or being killed. Only an
|
|
433
433
|
* answered non-zero exit is a real "no" ("not a repository", "no such path",
|
|
434
434
|
* "no such ref"); an unanswered call is NOT KNOWING, and a local-git step that
|
|
435
|
-
* does not know must not read as the permissive answer
|
|
435
|
+
* does not know must not read as the permissive answer.
|
|
436
436
|
*
|
|
437
437
|
* @param {CallResult} r
|
|
438
438
|
* @returns {boolean}
|
|
@@ -621,7 +621,7 @@ function lsRemoteDefaultBranch(ctx, root) {
|
|
|
621
621
|
* unreadable git did not answer, or origin/HEAD exists but its answer names
|
|
622
622
|
* no safe branch (another remote, a hostile or unparseable name).
|
|
623
623
|
* Either is a failure, never the residual case: gatherFacts reads
|
|
624
|
-
* it as an invalid base, which resolves required
|
|
624
|
+
* it as an invalid base, which resolves required.
|
|
625
625
|
* resolve-settings' defaultBranchCompliance fails the same answer
|
|
626
626
|
* closed, to `generic`.
|
|
627
627
|
*
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// publication can only lower it (D-PUBLICATION-CEILING) — a branch's
|
|
36
36
|
// `reviewPublication: "full"` raises nothing, since only a personal value asks
|
|
37
37
|
// for more than `auto`. The tracker (provider, site, key) is taken as the
|
|
38
|
-
// branch states it. It WRITES NOTHING
|
|
38
|
+
// branch states it. It WRITES NOTHING: each layer it folds is changed only by its owner.
|
|
39
39
|
//
|
|
40
40
|
// stdout is exactly one line plus "\n", or empty (D-SETTINGS-LINE):
|
|
41
41
|
// TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default>
|
|
@@ -750,7 +750,7 @@ function readRepoLayers(root, deps) {
|
|
|
750
750
|
* (D-LENS-UNION, D-SETTINGS-LOCAL-ONLY): origin/HEAD names the branch D, and
|
|
751
751
|
* `refs/remotes/origin/<D>:.devflow/project.json` is read through the same
|
|
752
752
|
* parser. The lens only adds, so every state that cannot be read is a malformed
|
|
753
|
-
* declaration (`generic`), never "declares nothing"
|
|
753
|
+
* declaration (`generic`), never "declares nothing":
|
|
754
754
|
* absent origin/HEAD is not recorded, or D holds no project.json
|
|
755
755
|
* malformed a git call did not answer, origin/HEAD names no safe branch, the
|
|
756
756
|
* blob overflows its bound, the file is unreadable, or its
|
|
@@ -898,7 +898,7 @@ function settleOutcome(outcome) {
|
|
|
898
898
|
}
|
|
899
899
|
|
|
900
900
|
// ---------------------------------------------------------------------------
|
|
901
|
-
// The suggestion the CLI prints (never writes —
|
|
901
|
+
// The suggestion the CLI prints (never writes — the team commits it)
|
|
902
902
|
// ---------------------------------------------------------------------------
|
|
903
903
|
|
|
904
904
|
/**
|
|
@@ -459,7 +459,7 @@ function runCall(io, file, args, timeout, maxBuffer) {
|
|
|
459
459
|
/**
|
|
460
460
|
* Whether a call ran to completion and exited on its own. Only an answered exit
|
|
461
461
|
* is a real "no"; a call that never started, timed out, overflowed or was refused
|
|
462
|
-
* is NOT KNOWING, and never reads as the permissive answer
|
|
462
|
+
* is NOT KNOWING, and never reads as the permissive answer.
|
|
463
463
|
*
|
|
464
464
|
* @param {CallResult} r
|
|
465
465
|
* @returns {boolean}
|
|
@@ -1530,7 +1530,7 @@ function gateEvidenceLine(deps, fields, total) {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
1532
|
// ---------------------------------------------------------------------------
|
|
1533
|
-
// splice — the compare-and-swap (D-VERIFY-CAS
|
|
1533
|
+
// splice — the compare-and-swap (D-VERIFY-CAS)
|
|
1534
1534
|
// ---------------------------------------------------------------------------
|
|
1535
1535
|
|
|
1536
1536
|
/**
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apply-decisions
|
|
3
|
-
description: Canonical algorithm for consuming DECISIONS_CONTEXT index — scan index, identify relevant entries, Read full bodies on demand, cite verbatim IDs
|
|
3
|
+
description: Canonical algorithm for consuming DECISIONS_CONTEXT index — scan index, identify relevant entries, Read full bodies on demand, cite verbatim IDs in-session and state the rule in words in anything committed or posted.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
allowed-tools: Read
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Apply Decisions
|
|
9
9
|
|
|
10
|
-
Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchestrators. The index lists each ADR/PF entry with ID
|
|
10
|
+
Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchestrators. The index lists each active ADR/PF entry with its ID and title — plus a status tag and an area on a v1 line, or the entry's scope on a v2 line — not the full body. Use this skill to surface the right decisions and pitfalls for your task without loading the entire corpus.
|
|
11
11
|
|
|
12
12
|
## Iron Law
|
|
13
13
|
|
|
@@ -23,35 +23,52 @@ Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchest
|
|
|
23
23
|
|
|
24
24
|
### Step 1: Scan the index
|
|
25
25
|
|
|
26
|
-
Read through all entries in `DECISIONS_CONTEXT`. The index
|
|
26
|
+
Read through all entries in `DECISIONS_CONTEXT`. The index lists active entries only, in two line kinds:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
Decisions (N):
|
|
30
|
-
ADR-
|
|
31
|
-
ADR-
|
|
30
|
+
ADR-NNN Return Result types from every fallible operation [Accepted]
|
|
31
|
+
ADR-NNN Claim the learning queue with an op, never with mv — src/assets/scripts/hooks/**, area:learning
|
|
32
32
|
|
|
33
33
|
Pitfalls (M):
|
|
34
|
-
PF-
|
|
35
|
-
PF-
|
|
34
|
+
PF-NNN Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs
|
|
35
|
+
PF-NNN A rename claims a shared file only while nothing re-creates it — area:hooks
|
|
36
36
|
|
|
37
37
|
ADR-NNN entries live in {worktree}/.devflow/learning/decisions.md
|
|
38
38
|
PF-NNN entries live in {worktree}/.devflow/learning/pitfalls.md
|
|
39
39
|
Read the relevant file and locate the matching `## ADR-NNN:` or `## PF-NNN:` heading for the full body.
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
- **A v1 line** ends in a status tag — `[Accepted]` on a decision, `[Active]` on a pitfall — after a title cut to 60 characters, and a pitfall adds its area after `—`.
|
|
43
|
+
- **A v2 line** has no tag. Its title is whole, and after `—` comes its scope: the globs and `area:` tags the rule governs, cut to 80 characters.
|
|
44
|
+
|
|
42
45
|
### Step 2: Identify plausibly-relevant entries
|
|
43
46
|
|
|
44
|
-
From the index, identify entries whose title or
|
|
45
|
-
- The files you are modifying or reviewing
|
|
47
|
+
From the index, identify entries whose title, area or scope plausibly overlaps with:
|
|
48
|
+
- The files you are modifying or reviewing — a v2 scope glob that matches one of them is a strong signal
|
|
46
49
|
- The category of issue you are addressing (e.g., error handling, hook scripts, JSON parsing)
|
|
47
50
|
- The architectural area of your change
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
A v1 title may be cut short — if a truncated title looks relevant, proceed to Step 3.
|
|
50
53
|
|
|
51
54
|
### Step 3: Read the full body
|
|
52
55
|
|
|
53
56
|
For each plausibly-relevant entry, use the Read tool to open the decisions file listed in the `DECISIONS_CONTEXT` footer and locate the matching `## ADR-NNN:` or `## PF-NNN:` heading. Read the full section to confirm relevance and understand the decision or pitfall completely.
|
|
54
57
|
|
|
58
|
+
A v2 body reads:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
## ADR-NNN: {title}
|
|
62
|
+
|
|
63
|
+
- **Status**: Accepted · verified {date}
|
|
64
|
+
- **Scope**: `{glob}`, `area:{tag}`
|
|
65
|
+
- **Decision**: {the rule}
|
|
66
|
+
- **Why**: {why it holds}
|
|
67
|
+
- **Source**: {where it was learned}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A pitfall's Status is `Active` and its rule line is labelled `**Rule**`. The verified date is the day the entry was last confirmed true; a body without one has not been confirmed since it was written. A v1 body carries `**Date**`, `**Status**`, `**Context**`, `**Decision**` and `**Consequences**` for a decision, or `**Area**`, `**Issue**`, `**Impact**`, `**Resolution**` and `**Status**` for a pitfall.
|
|
71
|
+
|
|
55
72
|
**The footer is the single source of truth for file paths.** Never substitute hardcoded paths — the footer resolves to the correct worktree, which may differ from your cwd in multi-worktree flows.
|
|
56
73
|
|
|
57
74
|
```
|
|
@@ -62,9 +79,11 @@ Use the exact paths from the DECISIONS_CONTEXT footer, e.g.:
|
|
|
62
79
|
|
|
63
80
|
Only cite an entry after you have read its full body and confirmed it applies.
|
|
64
81
|
|
|
65
|
-
### Step 4: Cite inline
|
|
82
|
+
### Step 4: Cite inline — in-session handoffs only
|
|
83
|
+
|
|
84
|
+
When applying a prior decision, cite as `applies ADR-NNN`. When avoiding a known pitfall, cite as `avoids PF-NNN`. Place these citations only where they stay in the session: your reasoning, decision tables, prompts to downstream agents, and your report back to the caller.
|
|
66
85
|
|
|
67
|
-
|
|
86
|
+
Anything committed, pushed or posted states the rule in words, never its ID: code, comments, tests, docs, KNOWLEDGE.md files, commit messages, PR and issue text, review and PR comments, resolution summaries — and any report a later step copies into one of those. The ledger is gitignored and numbered per machine, so no other clone can resolve an ID, and numbers get reused.
|
|
68
87
|
|
|
69
88
|
### Step 5: Use verbatim IDs only
|
|
70
89
|
|
|
@@ -76,17 +95,17 @@ Cite only IDs that appear verbatim in `DECISIONS_CONTEXT`. Do not guess at IDs t
|
|
|
76
95
|
|
|
77
96
|
**Scenario**: Reviewing `src/assets/scripts/hooks/background-learning` for issues.
|
|
78
97
|
|
|
79
|
-
1. **Scan** — Index shows `PF-
|
|
98
|
+
1. **Scan** — Index shows `PF-NNN Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs`
|
|
80
99
|
2. **Identify** — Area field includes `src/assets/scripts/hooks/` which overlaps with the file under review
|
|
81
|
-
3. **Read** — Open the pitfalls file at the path given in the DECISIONS_CONTEXT footer (e.g., `<worktree>/.devflow/learning/pitfalls.md`), find `## PF-
|
|
82
|
-
4. **Cite** — If the file shows signs of the god-script pattern, note `avoids PF-
|
|
83
|
-
5. **Verbatim** — ID `PF-
|
|
100
|
+
3. **Read** — Open the pitfalls file at the path given in the DECISIONS_CONTEXT footer (e.g., `<worktree>/.devflow/learning/pitfalls.md`), find `## PF-NNN:` section, read full body
|
|
101
|
+
4. **Cite** — If the file shows signs of the god-script pattern, note `avoids PF-NNN` in reasoning; a comment you commit for the fix says why in words ("hooks stay thin dispatchers"), never the ID
|
|
102
|
+
5. **Verbatim** — ID `PF-NNN` appeared in the index; citation is valid
|
|
84
103
|
|
|
85
104
|
---
|
|
86
105
|
|
|
87
106
|
## Skip Guard
|
|
88
107
|
|
|
89
|
-
When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill entirely
|
|
108
|
+
When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill entirely — unless your agent instructions tell you to read the decisions index yourself, in which case the index you read is your `DECISIONS_CONTEXT`. Do not otherwise load decisions files independently. Do not speculate about what decisions or pitfalls might exist.
|
|
90
109
|
|
|
91
110
|
---
|
|
92
111
|
|
|
@@ -98,3 +117,4 @@ When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill en
|
|
|
98
117
|
| Avoiding a known pitfall | `avoids PF-NNN` |
|
|
99
118
|
| Entry not in index | (no citation — silence is correct) |
|
|
100
119
|
| Entry in index but not read yet | (no citation — read first) |
|
|
120
|
+
| Text that is committed, pushed or posted | (no ID — state the rule in words) |
|
|
@@ -140,8 +140,8 @@ ensure_docs_dir() { mkdir -p "$(get_docs_root)/.devflow/docs/$1"; }
|
|
|
140
140
|
| Resolve cmd | `.devflow/docs/reviews/{branch-slug}/{timestamp}/resolution-summary.md` | Written by /resolve orchestrator (Phase 5) |
|
|
141
141
|
| Code-review cmd | `.devflow/docs/reviews/{branch-slug}/.last-review-head` | Overwrites with HEAD SHA |
|
|
142
142
|
| Working Memory | `.devflow/memory/WORKING-MEMORY.md` | Overwrites (auto-maintained by Stop hook) |
|
|
143
|
-
| Decisions | `.devflow/learning/decisions.md` | Rendered from `decisions-ledger.jsonl` (active ADR-NNN
|
|
144
|
-
| Pitfalls | `.devflow/learning/pitfalls.md` | Rendered from `decisions-ledger.jsonl` (active PF-NNN
|
|
143
|
+
| Decisions | `.devflow/learning/decisions.md` | Rendered from `decisions-ledger.jsonl` (active ADR-NNN entries in full; inactive ones listed in its Inactive table) |
|
|
144
|
+
| Pitfalls | `.devflow/learning/pitfalls.md` | Rendered from `decisions-ledger.jsonl` (active PF-NNN entries in full; inactive ones listed in its Inactive table) |
|
|
145
145
|
| Design agent (via /plan) | `.devflow/docs/design/{ISSUE_ID}-{topic-slug}.{timestamp}.md` | Creates new design artifact |
|
|
146
146
|
| Research agent | `.devflow/docs/research/{topic-slug}/{timestamp}/{type}.md` | Creates new in timestamped dir |
|
|
147
147
|
| Synthesize agent (research) | `.devflow/docs/research/{topic-slug}/{timestamp}/research-summary.md` | Creates new in timestamped dir |
|
|
@@ -117,7 +117,7 @@ updated: {ISO date}
|
|
|
117
117
|
[Most important files with one-line descriptions]
|
|
118
118
|
|
|
119
119
|
## Related
|
|
120
|
-
[Links to
|
|
120
|
+
[Links to other feature knowledge entries and key source files — never an ADR/PF ID]
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### Category Templates
|
|
@@ -167,9 +167,10 @@ Bad: `"Integration stuff"`
|
|
|
167
167
|
|
|
168
168
|
Knowledge files must not exist in isolation. The **Related** section must link to:
|
|
169
169
|
- Other feature knowledge entries that cover related topics
|
|
170
|
-
- ADR/PF entries from DECISIONS_CONTEXT (if provided)
|
|
171
170
|
- Key source files referenced in the knowledge
|
|
172
171
|
|
|
172
|
+
A decision or pitfall from DECISIONS_CONTEXT that shapes this area is stated in words in the section it governs, never by its ADR/PF ID: KNOWLEDGE.md is git-tracked and shared with every clone, and ledger IDs resolve only on the machine that recorded them.
|
|
173
|
+
|
|
173
174
|
References should be bidirectional — when creating a new feature knowledge entry that relates to an existing one, note the connection.
|
|
174
175
|
|
|
175
176
|
---
|
|
@@ -230,7 +231,7 @@ Run through this before writing. If any check fails, go back and fix it.
|
|
|
230
231
|
- [ ] File stays under 500 lines (split if necessary)
|
|
231
232
|
|
|
232
233
|
**Connections:**
|
|
233
|
-
- [ ] Cross-references to related feature knowledge entries and ADR/PF
|
|
234
|
+
- [ ] Cross-references to related feature knowledge entries in Related section; decisions and pitfalls stated in words, with no ADR/PF ID anywhere in the file
|
|
234
235
|
- [ ] No isolated knowledge islands — file connects to the broader knowledge network
|
|
235
236
|
- [ ] Key source files listed in Key Files section
|
|
236
237
|
|
|
@@ -325,8 +326,8 @@ All constants use `SCREAMING_SNAKE_CASE` with a descriptive prefix. If a value c
|
|
|
325
326
|
|
|
326
327
|
## Related
|
|
327
328
|
|
|
328
|
-
-
|
|
329
|
-
-
|
|
329
|
+
- `.devflow/features/cli-commands/KNOWLEDGE.md` — the command side of the lib/command split: catching errors and routing them to `showError()`
|
|
330
|
+
- `src/commands/` — where each integration is wired into a command
|
|
330
331
|
````
|
|
331
332
|
|
|
332
333
|
---
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"Bash(docker run --pid=host*)",
|
|
93
93
|
"Bash(docker run --net=host*)",
|
|
94
94
|
"Bash(docker run*--privileged*)",
|
|
95
|
-
"Bash(docker run*-v
|
|
96
|
-
"Bash(docker run*--volume
|
|
97
|
-
"Bash(docker run*--volume
|
|
95
|
+
"Bash(docker run*-v /:/*)",
|
|
96
|
+
"Bash(docker run*--volume /:/*)",
|
|
97
|
+
"Bash(docker run*--volume=/:/*)",
|
|
98
98
|
"Bash(docker pull *)",
|
|
99
99
|
"Bash(docker image pull *)",
|
|
100
100
|
"Bash(docker rm *)",
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import * as p from '@clack/prompts';
|
|
3
|
-
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
4
|
-
import { loadAndCountObservations } from './observations.js';
|
|
5
|
-
/**
|
|
6
|
-
* @file observation-io.ts
|
|
7
|
-
*
|
|
8
|
-
* File I/O for observations and user-facing warnings.
|
|
9
|
-
* Bridges the pure data module (observations.ts) with the filesystem.
|
|
10
|
-
*
|
|
11
|
-
* The `.md` files are a pure render of the decisions ledger — never edit them
|
|
12
|
-
* directly. To change the status of a decision or pitfall, use the
|
|
13
|
-
* `retire-anchor` op in `json-helper.cjs`, which flips `decisions_status` on
|
|
14
|
-
* the ledger row and re-renders both `.md` files atomically.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Read and parse observations from a log file.
|
|
18
|
-
* Returns empty results if the file does not exist.
|
|
19
|
-
*/
|
|
20
|
-
export async function readObservations(logPath) {
|
|
21
|
-
try {
|
|
22
|
-
const logContent = await fs.readFile(logPath, 'utf-8');
|
|
23
|
-
return loadAndCountObservations(logContent);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return { observations: [], invalidCount: 0 };
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Write observations back to a log file atomically.
|
|
31
|
-
* Each observation is serialized as a JSON line. Uses a `.tmp` sibling + rename so
|
|
32
|
-
* concurrent readers (e.g. background-learning during a race) never observe a
|
|
33
|
-
* half-written file. Delegates to `writeFileAtomicExclusive` in fs-atomic.ts
|
|
34
|
-
* (D34/D39: canonical TS atomic-write helper).
|
|
35
|
-
*/
|
|
36
|
-
export async function writeObservations(logPath, observations) {
|
|
37
|
-
const lines = observations.map(o => JSON.stringify(o));
|
|
38
|
-
const content = lines.join('\n') + (lines.length ? '\n' : '');
|
|
39
|
-
await writeFileAtomicExclusive(logPath, content);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Warn the user if invalid entries were found in a log file.
|
|
43
|
-
* Invalid entries are cleaned up automatically by the background curation pass.
|
|
44
|
-
*/
|
|
45
|
-
export function warnIfInvalid(invalidCount) {
|
|
46
|
-
if (invalidCount > 0) {
|
|
47
|
-
p.log.warn(`Note: ${invalidCount} invalid entry(ies) found. They will be cleaned up automatically.`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=observation-io.js.map
|