@jenga-ai/agent 3.1.1 → 3.2.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/agents/developer.md +15 -15
- package/agents/scrum-master.md +17 -17
- package/agents/tester.md +25 -15
- package/lib/skill-allow-list.json +3 -2
- package/package.json +1 -1
- package/scripts/audit-twin-divergence.sh +625 -0
- package/scripts/check-public-playbook-steps.sh +136 -0
- package/skills/j-close-story/SKILL.md +1 -1
- package/skills/j-do/SKILL.md +19 -19
- package/skills/j-doc-sync/SKILL.md +12 -1
- package/skills/j-idea/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +5 -4
- package/skills/j-init/assets/directory_structure.txt +1 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
- package/skills/j-init/scripts/init.sh +13 -2
- package/skills/j-playbook/SKILL.md +81 -0
- package/skills/j-proceed/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +29 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +3 -0
- package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
- package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
- package/skills/j-reconcile/SKILL.md +1 -0
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +12 -0
- package/skills/j-todo/SKILL.md +2 -2
- package/skills/j-uncharted/SKILL.md +8 -7
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
- package/skills/jenga/SKILL.md +55 -16
- package/skills/jenga/playbooks/idea-to-committed.json +20 -0
- package/skills/jenga/playbooks/schema.json +1 -1
- package/skills/jenga/scripts/load-playbooks.sh +855 -27
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/skills/jenga/scripts/render-playbook-confirmation.sh +162 -8
- package/skills/jenga/scripts/run-playbook-step.sh +535 -42
- package/skills/jenga-permission-level/SKILL.md +4 -4
- package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
- package/templates/playbook-types.json +8 -0
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +0 -22
|
@@ -9,72 +9,274 @@
|
|
|
9
9
|
# Implementation Principle, is the deterministic bookkeeping around that: which step is current,
|
|
10
10
|
# which have completed, which have failed, and enforcing that a failure halts the sequence
|
|
11
11
|
# permanently with no silent skip-ahead — exactly the story's acceptance criterion for mid-chain
|
|
12
|
-
# failure handling.
|
|
12
|
+
# failure handling. Conditional predicate EVALUATION (E53_S04_T02) is the same kind of
|
|
13
|
+
# deterministic bookkeeping — it belongs here too, never as inline `SKILL.md` reasoning.
|
|
13
14
|
#
|
|
14
|
-
# `/jenga`'s natural-language branch (wired in E53_S02_T04) uses this
|
|
15
|
-
# `render-playbook-confirmation.sh` (E53_S02_T03) returns a `confirmed`
|
|
15
|
+
# `/jenga`'s natural-language branch (wired in E53_S02_T04, extended by E53_S04_T07) uses this
|
|
16
|
+
# script as follows, after `render-playbook-confirmation.sh` (E53_S02_T03) returns a `confirmed`
|
|
17
|
+
# result:
|
|
16
18
|
#
|
|
17
|
-
# 1. `init` with the confirmed, ordered step list
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
19
|
+
# 1. `init` with the confirmed, ordered step list (and, optionally, per-step conditional
|
|
20
|
+
# metadata — see CONDITIONALS below) -> get the first step to invoke.
|
|
21
|
+
# 2. `should-skip <state_file>` -> deterministically decide whether the CURRENT step should run.
|
|
22
|
+
# - `{"skip": true, ...}` -> do NOT invoke the step; call `advance <state_file> skipped`.
|
|
23
|
+
# - `{"skip": false, ...}` -> invoke the step (as `/route`'s Step 6 already does for a single
|
|
24
|
+
# matched skill), then call `advance <state_file> passed ["<typed-output-value>"]` (step
|
|
25
|
+
# succeeded) or `advance <state_file> failed [note]` (step failed).
|
|
26
|
+
# 3. Any of the three `advance` outcomes returns the next step, a "complete" signal, or (on
|
|
27
|
+
# failure) a halt report.
|
|
21
28
|
# 4. Repeat 2-3 until "complete" or a halt report is returned.
|
|
22
29
|
#
|
|
30
|
+
# A `forward_from` step (E53_S03) resolves its actual invocation input via
|
|
31
|
+
# `get-output <state_file> <step_name>` (see GET-OUTPUT below) before being invoked in step 2.
|
|
32
|
+
#
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# CAPTURED TYPED-OUTPUT ARTIFACTS (E53_S04_T01)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# `forward_from` (E53_S03) and conditional step execution (E53_S04_T02) both need a single,
|
|
37
|
+
# shared record of "what typed output did step X actually produce when it ran" -- this is that
|
|
38
|
+
# record, and there is no second, separate capture path. Every step that produces a forwardable
|
|
39
|
+
# typed output value (per its own `output_types` declaration, see `docs/skill-authoring.md`)
|
|
40
|
+
# reports that value on its `advance ... passed` call; this script stores it in the state file's
|
|
41
|
+
# `captured_outputs` map, keyed by the step name that just completed. A step with no typed output
|
|
42
|
+
# to report simply omits the argument -- `captured_outputs` only ever gains entries for steps that
|
|
43
|
+
# actually supplied a value. A `skipped` step (see CONDITIONALS below) never runs, so it can never
|
|
44
|
+
# populate `captured_outputs` for itself either -- see GET-OUTPUT for the defined failure mode this
|
|
45
|
+
# causes for a downstream `forward_from` that names a skipped step.
|
|
46
|
+
#
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# CONDITIONALS AND THE `skipped` STATUS (E53_S04_T02)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# A step may declare a conditional predicate, evaluated against a NAMED PRIOR STEP's captured
|
|
51
|
+
# typed-output artifact (the same `captured_outputs` map above), that determines whether it
|
|
52
|
+
# executes. `init`'s optional 4th argument carries this metadata (see USAGE); a step absent from
|
|
53
|
+
# that map has no conditional and always runs, exactly as before this task.
|
|
54
|
+
#
|
|
55
|
+
# PREDICATE GRAMMAR (the simplest grammar that satisfies "evaluated against a named prior step's
|
|
56
|
+
# captured typed-output artifact" -- documented here as the single source of truth; playbook
|
|
57
|
+
# authors declare it via `load-playbooks.sh`'s StepObject `conditional` field, see that script's
|
|
58
|
+
# own header):
|
|
59
|
+
#
|
|
60
|
+
# non_empty true if the depended-on step's captured output is a non-empty string
|
|
61
|
+
# empty true if the depended-on step's captured output is absent or an empty string
|
|
62
|
+
# equals:<value> true if the captured output string equals <value> exactly
|
|
63
|
+
# not_equals:<value> true if the captured output string does NOT equal <value> exactly
|
|
64
|
+
#
|
|
65
|
+
# A depended-on step with NO captured output at all (never ran, ran but reported no value, or was
|
|
66
|
+
# itself skipped) is treated as an empty string for evaluation purposes -- `empty` matches,
|
|
67
|
+
# `non_empty` does not, `equals:<anything-nonblank>` does not, `not_equals:<anything-nonblank>`
|
|
68
|
+
# does.
|
|
69
|
+
#
|
|
70
|
+
# `should-skip <state_file>` evaluates the CURRENT step's conditional (if any) and reports whether
|
|
71
|
+
# it should be skipped, WITHOUT mutating the state file or advancing anything -- pure evaluation.
|
|
72
|
+
# The calling agent then either invokes the step normally, or calls `advance <state_file> skipped`
|
|
73
|
+
# directly without ever invoking the step. A `skipped` step is recorded in a `skipped` list
|
|
74
|
+
# (distinct from `completed`), advances the pointer exactly like `passed`, and NEVER halts the
|
|
75
|
+
# chain -- it is non-blocking, non-failing, per the story's explicit requirement.
|
|
76
|
+
#
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# COMPOSED/NESTED STEP HANDLING (E53_S05_T04)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# A composed/nested playbook chain (built by `load-playbooks.sh`'s composition resolution,
|
|
81
|
+
# E53_S05_T01) arrives here already fully flattened into ONE ordered `steps` list before it is ever
|
|
82
|
+
# passed to `init` -- this script never sees a raw `{"playbook": ...}` reference, only a flat,
|
|
83
|
+
# comma-separated list of step names, exactly as it already did before this story. Consequently the
|
|
84
|
+
# sequencing/halt state machine required ZERO behavioral changes: it was already format-agnostic to
|
|
85
|
+
# where each step name originated.
|
|
86
|
+
#
|
|
87
|
+
# The only addition is bookkeeping/traceability: `init`'s optional 5th argument (`<json-origins>`,
|
|
88
|
+
# see USAGE) stores per-step `{"playbook_id": ..., "depth": N}` metadata verbatim in the state
|
|
89
|
+
# file's `origins` map. This map is a PURE PASSTHROUGH -- no subcommand in this script (`init`,
|
|
90
|
+
# `should-skip`, `advance`, `get-output`) ever reads or branches on it. In particular:
|
|
91
|
+
# - HALT-ON-FAILURE is origin-agnostic: `advance <state_file> failed` halts the entire chain
|
|
92
|
+
# based purely on `current_index` -- a step's origin plays no role in this decision. Verified
|
|
93
|
+
# against a manually constructed composed scenario (three steps, the middle one tagged with
|
|
94
|
+
# `origins` metadata, `failed` called on it): the chain halted exactly as it would for an
|
|
95
|
+
# un-composed chain, with `failed_step` correctly naming the composed step and `never_run`
|
|
96
|
+
# correctly listing everything after it.
|
|
97
|
+
# - REPORTING SHAPES are unaffected: `completed`/`skipped`/`failed_step`/`failed_note`/`never_run`
|
|
98
|
+
# never include or omit anything based on `origins` -- their shape is identical whether or not
|
|
99
|
+
# any step in the run has an `origins` entry.
|
|
100
|
+
# - `should-skip`/`get-output`/`captured_outputs` addressing is unaffected: all three already key
|
|
101
|
+
# purely off step NAME (`current_step`/`step_name`), never consulting `origins` at all.
|
|
102
|
+
#
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# ARTIFACT PERSISTENCE AND REDACTION POLICY (E53_S04_T05)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Every captured typed-output VALUE (a `passed` advance call that supplies one -- the exact same
|
|
107
|
+
# data `captured_outputs` holds, no new data captured by this policy) is ALSO persisted to a
|
|
108
|
+
# per-run log file, separate from the ephemeral temp state file:
|
|
109
|
+
#
|
|
110
|
+
# project/logs/playbook-runs/<run_id>/artifacts.jsonl
|
|
111
|
+
#
|
|
112
|
+
# `<run_id>` (`<UTC-timestamp>-<playbook_id>-<8-hex>`) is generated once at `init` and stored in
|
|
113
|
+
# the state file; every `advance` call re-derives the same directory path from it. This reuses this
|
|
114
|
+
# repository's existing per-run/per-event JSON-LINES logging convention (the same append-one-line
|
|
115
|
+
# format `project/queue/scrum_triggers.jsonl` already uses) rather than inventing a new format.
|
|
116
|
+
# Each line is one JSON object: `{"step": "<name>", "captured_at": "<iso8601>", "value":
|
|
117
|
+
# "<redacted-or-original>"}`.
|
|
118
|
+
#
|
|
119
|
+
# PROJECT ROOT RESOLUTION mirrors `scripts/write-context-digest.sh`'s existing probing order:
|
|
120
|
+
# `JENGA_PROJECT_DIR` -> `CLAUDE_PROJECT_DIR` -> `git rev-parse --show-toplevel` -> `pwd`.
|
|
121
|
+
#
|
|
122
|
+
# TESTING OVERRIDE -- `JENGA_PLAYBOOK_RUNS_TEST_ROOT`: when set, overrides the project-root
|
|
123
|
+
# resolution above for this persistence path only (mirrors `load-playbooks.sh`'s own
|
|
124
|
+
# `JENGA_PLAYBOOKS_TEST_ROOT` convention). Fixture tests always set this to a throwaway
|
|
125
|
+
# `$BATS_TEST_TMPDIR` location -- never to this repository's own `project/logs/`.
|
|
126
|
+
#
|
|
127
|
+
# REDACTION (narrow, documented, BEST-EFFORT -- not exhaustive, not a guarantee) is applied ONLY to
|
|
128
|
+
# the PERSISTED copy, before it is written to `artifacts.jsonl`:
|
|
129
|
+
# - an absolute path OUTSIDE the resolved project root -> replaced with `[REDACTED_ABS_PATH]`
|
|
130
|
+
# - an email-address-shaped substring -> replaced with `[REDACTED_EMAIL]`
|
|
131
|
+
# - a phone-number-shaped digit sequence -> replaced with `[REDACTED_PHONE]`
|
|
132
|
+
# The LIVE `captured_outputs` value inside the temp state file is NEVER redacted -- `should-skip`
|
|
133
|
+
# and `get-output` need the real, unredacted value to evaluate predicates and resolve
|
|
134
|
+
# `forward_from` correctly. Redaction is a persistence-time-only transform on a separate copy of
|
|
135
|
+
# the data, never a mutation of the functional state.
|
|
136
|
+
#
|
|
137
|
+
# RETENTION/ROTATION POSTURE: identical to `project/logs/events.json`'s own actual current
|
|
138
|
+
# posture -- append-only, unbounded growth, no automatic deletion or rotation of old run
|
|
139
|
+
# directories. This is a deliberate, documented BEST-EFFORT policy, not a guarantee of bounded
|
|
140
|
+
# storage or of redaction completeness -- a narrow initial pattern set will not catch every
|
|
141
|
+
# possible sensitive value.
|
|
142
|
+
#
|
|
23
143
|
# ---------------------------------------------------------------------------
|
|
24
144
|
# USAGE
|
|
25
145
|
# ---------------------------------------------------------------------------
|
|
26
|
-
# run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated confirmed step names>"
|
|
146
|
+
# run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated confirmed step names>" ["<json-conditionals>" ["<json-origins>"]]
|
|
27
147
|
# Starts a new run. Creates a state file tracking the ordered step list, a current-step
|
|
28
|
-
# pointer (starts at the first step),
|
|
29
|
-
#
|
|
148
|
+
# pointer (starts at the first step), an empty `captured_outputs` map, empty
|
|
149
|
+
# completed/skipped/failed lists, and (if given) per-step conditional metadata and origin
|
|
150
|
+
# metadata. Emits the first step's info as JSON on stdout and a `STATE_FILE:` path on stderr.
|
|
151
|
+
#
|
|
152
|
+
# `<json-conditionals>` is OPTIONAL. When given, it is a JSON object mapping step name ->
|
|
153
|
+
# `{"depends_on": "<earlier step name>", "predicate": "<predicate>"}` for ONLY the steps
|
|
154
|
+
# that carry a conditional (steps absent from the object always run). Example:
|
|
155
|
+
# '{"stepC": {"depends_on": "stepA", "predicate": "non_empty"}}'
|
|
156
|
+
# Omitting this argument entirely (a 3-arg `init` call, E53_S04_T01's original form) means no
|
|
157
|
+
# step in this run carries a conditional -- fully backward compatible.
|
|
30
158
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
159
|
+
# `<json-origins>` is OPTIONAL (E53_S05_T04), and may only be given when `<json-conditionals>`
|
|
160
|
+
# is also given (pass `"{}"` for conditionals if there are none, to reach the 5th slot). It is
|
|
161
|
+
# a JSON object mapping a composed/nested step's name -> `{"playbook_id": "...", "depth": N}`,
|
|
162
|
+
# mirroring `render-playbook-confirmation.sh`'s own `<json-origins>` argument (E53_S05_T03).
|
|
163
|
+
# Stored verbatim in the state file's `origins` map -- a PURE PASSTHROUGH annotation, never
|
|
164
|
+
# read or branched on by ANY subcommand in this script (see "COMPOSED/NESTED STEP HANDLING"
|
|
165
|
+
# below). Omitting this argument reproduces the exact pre-this-task behavior, fully backward
|
|
166
|
+
# compatible.
|
|
167
|
+
#
|
|
168
|
+
# run-playbook-step.sh should-skip <state_file>
|
|
169
|
+
# Deterministically evaluates the CURRENT step's conditional against `captured_outputs`.
|
|
170
|
+
# Does NOT mutate the state file or advance the pointer -- pure evaluation, safe to call
|
|
171
|
+
# repeatedly. Emits, on stdout:
|
|
172
|
+
# {"skip": false, "step": "<name>"} (no conditional, or it evaluated true)
|
|
173
|
+
# {"skip": true, "step": "<name>", "depends_on": "...", "predicate": "...", "observed_value": "..." }
|
|
174
|
+
#
|
|
175
|
+
# run-playbook-step.sh advance <state_file> passed ["<typed-output-value>"]
|
|
176
|
+
# Records the CURRENT step as completed and advances the pointer. If a typed-output value is
|
|
177
|
+
# given, it is stored in `captured_outputs[<current step name>]` before advancing -- this
|
|
178
|
+
# argument is OPTIONAL; omitting it (a step with no declared output type) leaves
|
|
179
|
+
# `captured_outputs` untouched for that step and behaves exactly as before this argument
|
|
180
|
+
# existed. If more steps remain, emits the next step's info as JSON (same shape as `init`'s
|
|
181
|
+
# stdout). If that was the last step, emits a completion report instead (see OUTPUT SCHEMA)
|
|
182
|
+
# and removes the state file.
|
|
183
|
+
#
|
|
184
|
+
# run-playbook-step.sh advance <state_file> skipped
|
|
185
|
+
# Records the CURRENT step as SKIPPED (a new `skipped` list, distinct from `completed`) and
|
|
186
|
+
# advances the pointer exactly like `passed` -- a skipped step never halts the chain and is
|
|
187
|
+
# treated as non-blocking, non-failing. No typed-output value is ever accepted for `skipped`
|
|
188
|
+
# (the step never ran, so it has nothing to report). Same next-step/`complete` result shape
|
|
189
|
+
# as `passed`, except the `complete`/`step_ready` bookkeeping now also reflects the `skipped`
|
|
190
|
+
# list (see OUTPUT SCHEMA).
|
|
35
191
|
#
|
|
36
192
|
# run-playbook-step.sh advance <state_file> failed ["<note>"]
|
|
37
193
|
# Records the CURRENT step as failed (optionally with a free-text note) and halts the
|
|
38
194
|
# sequence PERMANENTLY — the state file is marked `halted: true` rather than removed, so a
|
|
39
195
|
# further `advance` call against it is rejected (see EXIT CODES). Emits a halt report (see
|
|
40
|
-
# OUTPUT SCHEMA) listing completed / failed / never-run steps.
|
|
196
|
+
# OUTPUT SCHEMA) listing completed / skipped / failed / never-run steps. (The optional 3rd
|
|
197
|
+
# argument means something different per outcome: a typed-output VALUE for `passed`, a
|
|
198
|
+
# free-text NOTE for `failed`, and is not accepted at all for `skipped` -- never ambiguous,
|
|
199
|
+
# since only one outcome word is given per call.)
|
|
200
|
+
#
|
|
201
|
+
# run-playbook-step.sh get-output <state_file> <step_name>
|
|
202
|
+
# Looks up `captured_outputs[<step_name>]` for a `forward_from` step to resolve its actual
|
|
203
|
+
# invocation input. Emits, on stdout, one of:
|
|
204
|
+
# {"status": "found", "step": "<step_name>", "value": "<value>"}
|
|
205
|
+
# {"status": "unavailable", "step": "<step_name>", "reason": "step_skipped"}
|
|
206
|
+
# {"status": "unavailable", "step": "<step_name>", "reason": "not_captured"}
|
|
207
|
+
# `reason: step_skipped` is the DEFINED, DOCUMENTED failure mode for "a skipped step followed
|
|
208
|
+
# by a step that attempts to forward_from it" (E53_S04_T06 fixture-covers this exact case) --
|
|
209
|
+
# this is never silently treated as an empty string; the calling agent must decide how to
|
|
210
|
+
# handle an unavailable forward source (e.g. halt the chain via `advance ... failed`).
|
|
211
|
+
# `reason: not_captured` covers every other case: the named step hasn't run yet, ran but
|
|
212
|
+
# reported no typed-output value, or does not exist in this run's step list at all.
|
|
41
213
|
#
|
|
42
214
|
# ---------------------------------------------------------------------------
|
|
43
215
|
# OUTPUT SCHEMA
|
|
44
216
|
# ---------------------------------------------------------------------------
|
|
45
|
-
# `init` and a `passed` `advance` call that has more steps remaining both emit, on
|
|
217
|
+
# `init` and a `passed`/`skipped` `advance` call that has more steps remaining both emit, on
|
|
218
|
+
# stdout:
|
|
46
219
|
#
|
|
47
220
|
# {"status": "step_ready", "step": "<skill name>", "step_index": 2, "total_steps": 5}
|
|
48
221
|
#
|
|
49
|
-
# A `passed` `advance` call on the FINAL step emits, on stdout (state file removed):
|
|
222
|
+
# A `passed`/`skipped` `advance` call on the FINAL step emits, on stdout (state file removed):
|
|
50
223
|
#
|
|
51
224
|
# {"status": "complete", "playbook_id": "...", "name": "...",
|
|
52
|
-
# "completed": ["<step1>", "<step2>", ...]}
|
|
225
|
+
# "completed": ["<step1>", ...], "skipped": ["<step2>", ...]}
|
|
53
226
|
#
|
|
54
227
|
# A `failed` `advance` call emits, on stdout (state file retained, marked halted):
|
|
55
228
|
#
|
|
56
229
|
# {"status": "halted", "playbook_id": "...", "name": "...",
|
|
57
|
-
# "completed": ["<step1>", ...], "
|
|
58
|
-
# "never_run": ["<stepN+1>", ...]}
|
|
230
|
+
# "completed": ["<step1>", ...], "skipped": ["<step2>", ...], "failed_step": "<stepN>",
|
|
231
|
+
# "failed_note": "<note or null>", "never_run": ["<stepN+1>", ...]}
|
|
59
232
|
#
|
|
60
233
|
# Nothing else is ever written to stdout — errors/warnings go to stderr only.
|
|
61
234
|
#
|
|
235
|
+
# `captured_outputs` itself is internal state-file bookkeeping, never emitted directly on stdout by
|
|
236
|
+
# `init`/`advance` -- a consumer that needs a captured value (e.g. a `forward_from` step resolving
|
|
237
|
+
# its input) reads it back via `get-output` (see USAGE above).
|
|
238
|
+
#
|
|
62
239
|
# ---------------------------------------------------------------------------
|
|
63
240
|
# EXIT CODES
|
|
64
241
|
# ---------------------------------------------------------------------------
|
|
65
|
-
# 0 `init` succeeded; OR `
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
242
|
+
# 0 `init` succeeded; OR `should-skip` succeeded (regardless of its `skip` verdict — a `true`
|
|
243
|
+
# verdict is a normal, expected outcome, not an error); OR `advance passed`/`advance skipped`
|
|
244
|
+
# succeeded (whether it returned the next step or a "complete" report); OR `advance failed`
|
|
245
|
+
# succeeded in recording the halt (a "halted" report IS the expected, successful outcome of
|
|
246
|
+
# this call — exit 0, not an error); OR `get-output` succeeded (an "unavailable" result IS a
|
|
247
|
+
# normal, expected outcome of this call — exit 0, not an error; the calling agent decides how
|
|
248
|
+
# to react to unavailability)
|
|
249
|
+
# 2 usage error (missing/malformed arguments, unrecognized outcome word, malformed
|
|
250
|
+
# `<json-conditionals>`), or a real setup problem (python3 unavailable, state file
|
|
251
|
+
# missing/corrupt)
|
|
252
|
+
# 3 `advance`/`should-skip`/`get-output` called against a state file already marked `halted:
|
|
253
|
+
# true` from a prior `failed` call — rejected outright rather than silently resuming; this is
|
|
254
|
+
# the "no skip-ahead, no silent resumption after a halt" guard the story's acceptance
|
|
255
|
+
# criteria require
|
|
73
256
|
#
|
|
74
257
|
# ---------------------------------------------------------------------------
|
|
75
258
|
|
|
76
259
|
set -euo pipefail
|
|
77
260
|
|
|
261
|
+
# Resolve project root (E53_S04_T05, mirrors scripts/write-context-digest.sh's own probing order).
|
|
262
|
+
# JENGA_PLAYBOOK_RUNS_TEST_ROOT is a TEST-ONLY override for the artifact-persistence path -- never
|
|
263
|
+
# set it in a real invocation (see "ARTIFACT PERSISTENCE AND REDACTION POLICY" above).
|
|
264
|
+
resolve_project_dir() {
|
|
265
|
+
if [ -n "${JENGA_PLAYBOOK_RUNS_TEST_ROOT:-}" ]; then
|
|
266
|
+
printf '%s\n' "$JENGA_PLAYBOOK_RUNS_TEST_ROOT"
|
|
267
|
+
return 0
|
|
268
|
+
fi
|
|
269
|
+
if [ -n "${JENGA_PROJECT_DIR:-}" ]; then
|
|
270
|
+
printf '%s\n' "$JENGA_PROJECT_DIR"
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
274
|
+
printf '%s\n' "$CLAUDE_PROJECT_DIR"
|
|
275
|
+
return 0
|
|
276
|
+
fi
|
|
277
|
+
git rev-parse --show-toplevel 2>/dev/null || pwd
|
|
278
|
+
}
|
|
279
|
+
|
|
78
280
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
79
281
|
echo "Error: python3 is required by run-playbook-step.sh" >&2
|
|
80
282
|
exit 2
|
|
@@ -82,9 +284,12 @@ fi
|
|
|
82
284
|
|
|
83
285
|
if [ $# -lt 1 ]; then
|
|
84
286
|
echo "Usage:" >&2
|
|
85
|
-
echo " run-playbook-step.sh init \"<playbook_id>\" \"<name>\" \"<comma-separated confirmed step names>\"" >&2
|
|
86
|
-
echo " run-playbook-step.sh
|
|
287
|
+
echo " run-playbook-step.sh init \"<playbook_id>\" \"<name>\" \"<comma-separated confirmed step names>\" [\"<json-conditionals>\" [\"<json-origins>\"]]" >&2
|
|
288
|
+
echo " run-playbook-step.sh should-skip <state_file>" >&2
|
|
289
|
+
echo " run-playbook-step.sh advance <state_file> passed [\"<typed-output-value>\"]" >&2
|
|
290
|
+
echo " run-playbook-step.sh advance <state_file> skipped" >&2
|
|
87
291
|
echo " run-playbook-step.sh advance <state_file> failed [\"<note>\"]" >&2
|
|
292
|
+
echo " run-playbook-step.sh get-output <state_file> <step_name>" >&2
|
|
88
293
|
exit 2
|
|
89
294
|
fi
|
|
90
295
|
|
|
@@ -92,13 +297,15 @@ SUBCOMMAND="$1"
|
|
|
92
297
|
shift
|
|
93
298
|
|
|
94
299
|
if [ "$SUBCOMMAND" = "init" ]; then
|
|
95
|
-
if [ $# -
|
|
96
|
-
echo 'Usage: run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated steps>"' >&2
|
|
300
|
+
if [ $# -lt 3 ] || [ $# -gt 5 ]; then
|
|
301
|
+
echo 'Usage: run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated steps>" ["<json-conditionals>" ["<json-origins>"]]' >&2
|
|
97
302
|
exit 2
|
|
98
303
|
fi
|
|
99
304
|
PLAYBOOK_ID="$1"
|
|
100
305
|
PLAYBOOK_NAME="$2"
|
|
101
306
|
RAW_STEPS="$3"
|
|
307
|
+
RAW_CONDITIONALS="${4:-}"
|
|
308
|
+
RAW_ORIGINS="${5:-}"
|
|
102
309
|
|
|
103
310
|
if [ -z "${RAW_STEPS// /}" ]; then
|
|
104
311
|
echo "Error: no steps given to run-playbook-step.sh init" >&2
|
|
@@ -112,6 +319,8 @@ if [ "$SUBCOMMAND" = "init" ]; then
|
|
|
112
319
|
|
|
113
320
|
cat > "$PY_SCRIPT" <<'PY'
|
|
114
321
|
import json
|
|
322
|
+
import re
|
|
323
|
+
import secrets
|
|
115
324
|
import sys
|
|
116
325
|
from datetime import datetime, timezone
|
|
117
326
|
|
|
@@ -119,20 +328,93 @@ state_file_path = sys.argv[1]
|
|
|
119
328
|
playbook_id = sys.argv[2]
|
|
120
329
|
playbook_name = sys.argv[3]
|
|
121
330
|
raw_steps = sys.argv[4]
|
|
331
|
+
raw_conditionals = sys.argv[5] if len(sys.argv) > 5 and sys.argv[5] != "" else None
|
|
332
|
+
raw_origins = sys.argv[6] if len(sys.argv) > 6 and sys.argv[6] != "" else None
|
|
122
333
|
|
|
123
334
|
steps = [s.strip() for s in raw_steps.split(",") if s.strip() != ""]
|
|
124
335
|
if not steps:
|
|
125
336
|
print("Error: no valid step names parsed from the given list", file=sys.stderr)
|
|
126
337
|
sys.exit(2)
|
|
127
338
|
|
|
339
|
+
PREDICATE_RE = re.compile(r'^(non_empty|empty|equals:.+|not_equals:.+)$')
|
|
340
|
+
|
|
341
|
+
conditionals = {}
|
|
342
|
+
if raw_conditionals is not None:
|
|
343
|
+
try:
|
|
344
|
+
parsed = json.loads(raw_conditionals)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"Error: <json-conditionals> is not valid JSON: {e}", file=sys.stderr)
|
|
347
|
+
sys.exit(2)
|
|
348
|
+
if not isinstance(parsed, dict):
|
|
349
|
+
print("Error: <json-conditionals> must be a JSON object", file=sys.stderr)
|
|
350
|
+
sys.exit(2)
|
|
351
|
+
for step_name, cond in parsed.items():
|
|
352
|
+
if step_name not in steps:
|
|
353
|
+
print(f"Error: conditional given for '{step_name}', which is not in the step list", file=sys.stderr)
|
|
354
|
+
sys.exit(2)
|
|
355
|
+
if not isinstance(cond, dict) or "depends_on" not in cond or "predicate" not in cond:
|
|
356
|
+
print(f"Error: conditional for '{step_name}' must be an object with 'depends_on' and 'predicate'", file=sys.stderr)
|
|
357
|
+
sys.exit(2)
|
|
358
|
+
depends_on = cond["depends_on"]
|
|
359
|
+
predicate = cond["predicate"]
|
|
360
|
+
if depends_on not in steps or steps.index(depends_on) >= steps.index(step_name):
|
|
361
|
+
print(f"Error: conditional for '{step_name}' depends_on '{depends_on}', which is not an earlier step", file=sys.stderr)
|
|
362
|
+
sys.exit(2)
|
|
363
|
+
if not isinstance(predicate, str) or not PREDICATE_RE.match(predicate):
|
|
364
|
+
print(f"Error: conditional for '{step_name}' has an unrecognized predicate '{predicate}'", file=sys.stderr)
|
|
365
|
+
sys.exit(2)
|
|
366
|
+
conditionals[step_name] = {"depends_on": depends_on, "predicate": predicate}
|
|
367
|
+
|
|
368
|
+
# --- E53_S05_T04: origin/nesting metadata -- a PURE PASSTHROUGH, never read by any subcommand in
|
|
369
|
+
# this script (see header 'COMPOSED/NESTED STEP HANDLING'). ---
|
|
370
|
+
origins = {}
|
|
371
|
+
if raw_origins is not None:
|
|
372
|
+
try:
|
|
373
|
+
parsed_origins = json.loads(raw_origins)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
print(f"Error: <json-origins> is not valid JSON: {e}", file=sys.stderr)
|
|
376
|
+
sys.exit(2)
|
|
377
|
+
if not isinstance(parsed_origins, dict):
|
|
378
|
+
print("Error: <json-origins> must be a JSON object", file=sys.stderr)
|
|
379
|
+
sys.exit(2)
|
|
380
|
+
for step_name, meta in parsed_origins.items():
|
|
381
|
+
if step_name not in steps:
|
|
382
|
+
print(f"Error: origin metadata given for '{step_name}', which is not in the step list", file=sys.stderr)
|
|
383
|
+
sys.exit(2)
|
|
384
|
+
if (
|
|
385
|
+
not isinstance(meta, dict)
|
|
386
|
+
or not isinstance(meta.get("playbook_id"), str)
|
|
387
|
+
or not meta.get("playbook_id")
|
|
388
|
+
or not isinstance(meta.get("depth"), int)
|
|
389
|
+
or isinstance(meta.get("depth"), bool)
|
|
390
|
+
or meta.get("depth") <= 1
|
|
391
|
+
):
|
|
392
|
+
print(
|
|
393
|
+
f"Error: origin metadata for '{step_name}' must be an object with a non-empty "
|
|
394
|
+
f"string 'playbook_id' and an integer 'depth' > 1",
|
|
395
|
+
file=sys.stderr,
|
|
396
|
+
)
|
|
397
|
+
sys.exit(2)
|
|
398
|
+
origins[step_name] = {"playbook_id": meta["playbook_id"], "depth": meta["depth"]}
|
|
399
|
+
|
|
400
|
+
# run_id (E53_S04_T05) -- identifies this run's persisted-artifact directory
|
|
401
|
+
# (project/logs/playbook-runs/<run_id>/), stored here so every later `advance` call (a fresh
|
|
402
|
+
# process) can re-derive the same path without needing it passed again.
|
|
403
|
+
run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{playbook_id}-{secrets.token_hex(4)}"
|
|
404
|
+
|
|
128
405
|
state = {
|
|
129
406
|
"version": 1,
|
|
130
407
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
131
408
|
"playbook_id": playbook_id,
|
|
132
409
|
"playbook_name": playbook_name,
|
|
410
|
+
"run_id": run_id,
|
|
133
411
|
"steps": steps,
|
|
134
412
|
"current_index": 0,
|
|
135
413
|
"completed": [],
|
|
414
|
+
"skipped": [],
|
|
415
|
+
"captured_outputs": {},
|
|
416
|
+
"conditionals": conditionals,
|
|
417
|
+
"origins": origins,
|
|
136
418
|
"halted": False,
|
|
137
419
|
"failed_step": None,
|
|
138
420
|
"failed_note": None,
|
|
@@ -151,20 +433,159 @@ print(json.dumps({
|
|
|
151
433
|
print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
|
|
152
434
|
PY
|
|
153
435
|
|
|
154
|
-
python3 "$PY_SCRIPT" "$STATE_FILE" "$PLAYBOOK_ID" "$PLAYBOOK_NAME" "$RAW_STEPS"
|
|
436
|
+
python3 "$PY_SCRIPT" "$STATE_FILE" "$PLAYBOOK_ID" "$PLAYBOOK_NAME" "$RAW_STEPS" "$RAW_CONDITIONALS" "$RAW_ORIGINS"
|
|
155
437
|
exit 0
|
|
156
438
|
|
|
439
|
+
elif [ "$SUBCOMMAND" = "should-skip" ]; then
|
|
440
|
+
if [ $# -ne 1 ]; then
|
|
441
|
+
echo 'Usage: run-playbook-step.sh should-skip <state_file>' >&2
|
|
442
|
+
exit 2
|
|
443
|
+
fi
|
|
444
|
+
STATE_FILE="$1"
|
|
445
|
+
|
|
446
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
447
|
+
echo "Error: state file not found at $STATE_FILE" >&2
|
|
448
|
+
exit 2
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
PY_SCRIPT="$(mktemp -t run-playbook-step-should-skip-XXXXXX.py)"
|
|
452
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
453
|
+
|
|
454
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
455
|
+
import json
|
|
456
|
+
import sys
|
|
457
|
+
|
|
458
|
+
state_file_path = sys.argv[1]
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
with open(state_file_path, encoding="utf-8") as f:
|
|
462
|
+
state = json.load(f)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
print(f"Error: could not read/parse state file at {state_file_path}: {e}", file=sys.stderr)
|
|
465
|
+
sys.exit(2)
|
|
466
|
+
|
|
467
|
+
if state.get("halted"):
|
|
468
|
+
print(
|
|
469
|
+
f"Error: this playbook run already halted on step '{state.get('failed_step')}'. "
|
|
470
|
+
"No further evaluation is meaningful after a halt.",
|
|
471
|
+
file=sys.stderr,
|
|
472
|
+
)
|
|
473
|
+
sys.exit(3)
|
|
474
|
+
|
|
475
|
+
steps = state["steps"]
|
|
476
|
+
idx = state["current_index"]
|
|
477
|
+
|
|
478
|
+
if idx >= len(steps):
|
|
479
|
+
print(f"Error: state file at {state_file_path} has no current step (already complete).", file=sys.stderr)
|
|
480
|
+
sys.exit(2)
|
|
481
|
+
|
|
482
|
+
current_step = steps[idx]
|
|
483
|
+
conditionals = state.get("conditionals", {})
|
|
484
|
+
cond = conditionals.get(current_step)
|
|
485
|
+
|
|
486
|
+
if not cond:
|
|
487
|
+
print(json.dumps({"skip": False, "step": current_step}))
|
|
488
|
+
sys.exit(0)
|
|
489
|
+
|
|
490
|
+
depends_on = cond["depends_on"]
|
|
491
|
+
predicate = cond["predicate"]
|
|
492
|
+
observed = state.get("captured_outputs", {}).get(depends_on, "")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def evaluate(predicate, value):
|
|
496
|
+
if predicate == "non_empty":
|
|
497
|
+
return bool(value)
|
|
498
|
+
if predicate == "empty":
|
|
499
|
+
return not bool(value)
|
|
500
|
+
if predicate.startswith("equals:"):
|
|
501
|
+
return value == predicate[len("equals:"):]
|
|
502
|
+
if predicate.startswith("not_equals:"):
|
|
503
|
+
return value != predicate[len("not_equals:"):]
|
|
504
|
+
# Load-time validation (load-playbooks.sh) and init's own validation above should have already
|
|
505
|
+
# rejected any other shape -- this is a defensive fallback, never expected to trigger.
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
predicate_true = evaluate(predicate, observed)
|
|
510
|
+
skip = not predicate_true
|
|
511
|
+
|
|
512
|
+
print(json.dumps({
|
|
513
|
+
"skip": skip,
|
|
514
|
+
"step": current_step,
|
|
515
|
+
"depends_on": depends_on,
|
|
516
|
+
"predicate": predicate,
|
|
517
|
+
"observed_value": observed,
|
|
518
|
+
}))
|
|
519
|
+
sys.exit(0)
|
|
520
|
+
PY
|
|
521
|
+
|
|
522
|
+
python3 "$PY_SCRIPT" "$STATE_FILE"
|
|
523
|
+
exit $?
|
|
524
|
+
|
|
525
|
+
elif [ "$SUBCOMMAND" = "get-output" ]; then
|
|
526
|
+
if [ $# -ne 2 ]; then
|
|
527
|
+
echo 'Usage: run-playbook-step.sh get-output <state_file> <step_name>' >&2
|
|
528
|
+
exit 2
|
|
529
|
+
fi
|
|
530
|
+
STATE_FILE="$1"
|
|
531
|
+
STEP_NAME="$2"
|
|
532
|
+
|
|
533
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
534
|
+
echo "Error: state file not found at $STATE_FILE" >&2
|
|
535
|
+
exit 2
|
|
536
|
+
fi
|
|
537
|
+
|
|
538
|
+
PY_SCRIPT="$(mktemp -t run-playbook-step-get-output-XXXXXX.py)"
|
|
539
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
540
|
+
|
|
541
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
542
|
+
import json
|
|
543
|
+
import sys
|
|
544
|
+
|
|
545
|
+
state_file_path = sys.argv[1]
|
|
546
|
+
step_name = sys.argv[2]
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
with open(state_file_path, encoding="utf-8") as f:
|
|
550
|
+
state = json.load(f)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
print(f"Error: could not read/parse state file at {state_file_path}: {e}", file=sys.stderr)
|
|
553
|
+
sys.exit(2)
|
|
554
|
+
|
|
555
|
+
captured = state.get("captured_outputs", {})
|
|
556
|
+
skipped = state.get("skipped", [])
|
|
557
|
+
|
|
558
|
+
if step_name in captured:
|
|
559
|
+
print(json.dumps({"status": "found", "step": step_name, "value": captured[step_name]}))
|
|
560
|
+
sys.exit(0)
|
|
561
|
+
|
|
562
|
+
if step_name in skipped:
|
|
563
|
+
print(json.dumps({"status": "unavailable", "step": step_name, "reason": "step_skipped"}))
|
|
564
|
+
sys.exit(0)
|
|
565
|
+
|
|
566
|
+
print(json.dumps({"status": "unavailable", "step": step_name, "reason": "not_captured"}))
|
|
567
|
+
sys.exit(0)
|
|
568
|
+
PY
|
|
569
|
+
|
|
570
|
+
python3 "$PY_SCRIPT" "$STATE_FILE" "$STEP_NAME"
|
|
571
|
+
exit $?
|
|
572
|
+
|
|
157
573
|
elif [ "$SUBCOMMAND" = "advance" ]; then
|
|
158
574
|
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
|
159
|
-
echo 'Usage: run-playbook-step.sh advance <state_file> passed|failed ["<note>"]' >&2
|
|
575
|
+
echo 'Usage: run-playbook-step.sh advance <state_file> passed|skipped|failed ["<value-or-note>"]' >&2
|
|
160
576
|
exit 2
|
|
161
577
|
fi
|
|
162
578
|
STATE_FILE="$1"
|
|
163
579
|
OUTCOME="$2"
|
|
164
|
-
|
|
580
|
+
EXTRA="${3:-}"
|
|
581
|
+
|
|
582
|
+
if [ "$OUTCOME" != "passed" ] && [ "$OUTCOME" != "skipped" ] && [ "$OUTCOME" != "failed" ]; then
|
|
583
|
+
echo "Error: outcome must be 'passed', 'skipped', or 'failed', got '$OUTCOME'" >&2
|
|
584
|
+
exit 2
|
|
585
|
+
fi
|
|
165
586
|
|
|
166
|
-
if [ "$OUTCOME"
|
|
167
|
-
echo "Error:
|
|
587
|
+
if [ "$OUTCOME" = "skipped" ] && [ -n "$EXTRA" ]; then
|
|
588
|
+
echo "Error: 'skipped' does not accept a 3rd argument -- a skipped step never ran and has nothing to report" >&2
|
|
168
589
|
exit 2
|
|
169
590
|
fi
|
|
170
591
|
|
|
@@ -174,17 +595,77 @@ elif [ "$SUBCOMMAND" = "advance" ]; then
|
|
|
174
595
|
exit 2
|
|
175
596
|
fi
|
|
176
597
|
|
|
598
|
+
PROJECT_DIR="$(resolve_project_dir)"
|
|
599
|
+
|
|
177
600
|
PY_SCRIPT="$(mktemp -t run-playbook-step-advance-XXXXXX.py)"
|
|
178
601
|
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
179
602
|
|
|
180
603
|
cat > "$PY_SCRIPT" <<'PY'
|
|
181
604
|
import json
|
|
182
605
|
import os
|
|
606
|
+
import re
|
|
183
607
|
import sys
|
|
608
|
+
from datetime import datetime, timezone
|
|
184
609
|
|
|
185
610
|
state_file_path = sys.argv[1]
|
|
186
611
|
outcome = sys.argv[2]
|
|
187
|
-
|
|
612
|
+
# The optional 3rd argument means something different per outcome: a typed-output VALUE for
|
|
613
|
+
# `passed` (E53_S04_T01), a free-text NOTE for `failed` -- never accepted for `skipped` (rejected
|
|
614
|
+
# by the bash-level guard above). Only one of the two is ever read below, selected by `outcome`.
|
|
615
|
+
extra = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != "" else None
|
|
616
|
+
note = extra
|
|
617
|
+
project_dir = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] != "" else None
|
|
618
|
+
|
|
619
|
+
# --- E53_S04_T05: artifact persistence + redaction (persisted copy only -- see header) ---
|
|
620
|
+
_ABS_PATH_RE = re.compile(r'(/[^\s"\']+)')
|
|
621
|
+
_EMAIL_RE = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
|
|
622
|
+
_PHONE_RE = re.compile(r'\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b')
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _redact(value, project_root):
|
|
626
|
+
"""Best-effort, narrow redaction of a value before it is PERSISTED -- never applied to the
|
|
627
|
+
live, functional captured_outputs value in the state file. See header 'ARTIFACT PERSISTENCE
|
|
628
|
+
AND REDACTION POLICY' for the full policy and its explicit best-effort framing."""
|
|
629
|
+
if not isinstance(value, str):
|
|
630
|
+
return value
|
|
631
|
+
|
|
632
|
+
def _replace_path(m):
|
|
633
|
+
p = m.group(1)
|
|
634
|
+
try:
|
|
635
|
+
abs_p = os.path.abspath(p)
|
|
636
|
+
abs_root = os.path.abspath(project_root)
|
|
637
|
+
if abs_p == abs_root or abs_p.startswith(abs_root + os.sep):
|
|
638
|
+
return p # inside the project root -- not redacted
|
|
639
|
+
except Exception:
|
|
640
|
+
pass
|
|
641
|
+
return "[REDACTED_ABS_PATH]"
|
|
642
|
+
|
|
643
|
+
redacted = _ABS_PATH_RE.sub(_replace_path, value) if project_root else value
|
|
644
|
+
redacted = _EMAIL_RE.sub("[REDACTED_EMAIL]", redacted)
|
|
645
|
+
redacted = _PHONE_RE.sub("[REDACTED_PHONE]", redacted)
|
|
646
|
+
return redacted
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _persist_artifact(state, step_name, value, project_root):
|
|
650
|
+
"""Append one redacted record to project/logs/playbook-runs/<run_id>/artifacts.jsonl.
|
|
651
|
+
Best-effort: any failure to persist (unwritable filesystem, missing run_id on an old state
|
|
652
|
+
file) is swallowed with a stderr warning -- persistence is a durability nicety, never a reason
|
|
653
|
+
to fail a playbook run that otherwise succeeded."""
|
|
654
|
+
run_id = state.get("run_id")
|
|
655
|
+
if not run_id or not project_root:
|
|
656
|
+
return
|
|
657
|
+
try:
|
|
658
|
+
run_dir = os.path.join(project_root, "project", "logs", "playbook-runs", run_id)
|
|
659
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
660
|
+
record = {
|
|
661
|
+
"step": step_name,
|
|
662
|
+
"captured_at": datetime.now(timezone.utc).isoformat(),
|
|
663
|
+
"value": _redact(value, project_root),
|
|
664
|
+
}
|
|
665
|
+
with open(os.path.join(run_dir, "artifacts.jsonl"), "a", encoding="utf-8") as f:
|
|
666
|
+
f.write(json.dumps(record) + "\n")
|
|
667
|
+
except OSError as e:
|
|
668
|
+
print(f"Warning: could not persist captured-output artifact for '{step_name}': {e}", file=sys.stderr)
|
|
188
669
|
|
|
189
670
|
try:
|
|
190
671
|
with open(state_file_path, encoding="utf-8") as f:
|
|
@@ -210,6 +691,7 @@ if idx >= len(steps):
|
|
|
210
691
|
sys.exit(2)
|
|
211
692
|
|
|
212
693
|
current_step = steps[idx]
|
|
694
|
+
state.setdefault("skipped", [])
|
|
213
695
|
|
|
214
696
|
if outcome == "failed":
|
|
215
697
|
state["halted"] = True
|
|
@@ -226,14 +708,24 @@ if outcome == "failed":
|
|
|
226
708
|
"playbook_id": state["playbook_id"],
|
|
227
709
|
"name": state["playbook_name"],
|
|
228
710
|
"completed": state["completed"],
|
|
711
|
+
"skipped": state["skipped"],
|
|
229
712
|
"failed_step": current_step,
|
|
230
713
|
"failed_note": note,
|
|
231
714
|
"never_run": never_run,
|
|
232
715
|
}))
|
|
233
716
|
sys.exit(0)
|
|
234
717
|
|
|
235
|
-
|
|
236
|
-
|
|
718
|
+
if outcome == "skipped":
|
|
719
|
+
# A skipped step never ran -- it is non-blocking, non-failing, and never captures an output.
|
|
720
|
+
state["skipped"].append(current_step)
|
|
721
|
+
else:
|
|
722
|
+
# outcome == "passed"
|
|
723
|
+
typed_output_value = extra
|
|
724
|
+
if typed_output_value is not None:
|
|
725
|
+
state["captured_outputs"][current_step] = typed_output_value
|
|
726
|
+
_persist_artifact(state, current_step, typed_output_value, project_dir)
|
|
727
|
+
state["completed"].append(current_step)
|
|
728
|
+
|
|
237
729
|
state["current_index"] = idx + 1
|
|
238
730
|
|
|
239
731
|
if state["current_index"] >= len(steps):
|
|
@@ -242,6 +734,7 @@ if state["current_index"] >= len(steps):
|
|
|
242
734
|
"playbook_id": state["playbook_id"],
|
|
243
735
|
"name": state["playbook_name"],
|
|
244
736
|
"completed": state["completed"],
|
|
737
|
+
"skipped": state["skipped"],
|
|
245
738
|
}
|
|
246
739
|
try:
|
|
247
740
|
os.remove(state_file_path)
|
|
@@ -264,10 +757,10 @@ print(json.dumps({
|
|
|
264
757
|
print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
|
|
265
758
|
PY
|
|
266
759
|
|
|
267
|
-
python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$
|
|
760
|
+
python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$EXTRA" "$PROJECT_DIR"
|
|
268
761
|
exit $?
|
|
269
762
|
|
|
270
763
|
else
|
|
271
|
-
echo "Error: unrecognized subcommand '$SUBCOMMAND' (expected 'init'
|
|
764
|
+
echo "Error: unrecognized subcommand '$SUBCOMMAND' (expected 'init', 'should-skip', 'advance', or 'get-output')" >&2
|
|
272
765
|
exit 2
|
|
273
766
|
fi
|