@zalom/plastic 1.0.0-alpha.9 → 1.0.0-beta.10
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/PLASTIC.md +170 -469
- package/README.md +95 -58
- package/agents/plastic-brainstorming.md +45 -0
- package/agents/plastic-enforcer.md +36 -0
- package/agents/plastic-executor.md +47 -0
- package/agents/{future-intent-researcher.md → plastic-future-intent-researcher.md} +1 -1
- package/agents/{intent-curator.md → plastic-intent-curator.md} +8 -6
- package/agents/plastic-planner.md +47 -0
- package/agents/plastic-spec-specialist.md +45 -0
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/deprecations.yml +1 -10
- package/hooks/auto-arm +5 -0
- package/hooks/bash-gate +3 -0
- package/hooks/check-update +12 -8
- package/hooks/code-gate +12 -0
- package/hooks/create-gate +3 -0
- package/hooks/gate-check +3 -1
- package/hooks/hooks.json +52 -0
- package/hooks/qmd-search +8 -0
- package/hooks/statusline +150 -41
- package/package.json +2 -2
- package/scripts/agent-report +142 -0
- package/scripts/dashboard.rb +687 -0
- package/scripts/doctor.rb +1219 -621
- package/scripts/hook-auto-arm +51 -0
- package/scripts/hook-bash-gate +41 -0
- package/scripts/hook-code-gate +27 -0
- package/scripts/hook-continue +15 -114
- package/scripts/hook-create-gate +59 -0
- package/scripts/hook-gate-check +47 -32
- package/scripts/hook-qmd-search +44 -0
- package/scripts/hook-session-start +106 -38
- package/scripts/install.rb +91 -529
- package/scripts/lib/boot_banner.rb +28 -0
- package/scripts/lib/bridge.rb +452 -19
- package/scripts/lib/frontmatter_writer.rb +130 -0
- package/scripts/lib/graph_rebuild.rb +328 -0
- package/scripts/lib/installer_core.rb +812 -0
- package/scripts/lib/intent_validator.rb +235 -0
- package/scripts/lib/links_projection.rb +160 -0
- package/scripts/lib/links_section.rb +207 -0
- package/scripts/lib/power_tools.rb +76 -0
- package/scripts/lib/qmd_hook.rb +57 -0
- package/scripts/lib/qmd_sync.rb +230 -0
- package/scripts/lib/store_provisioning.rb +100 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +327 -0
- package/scripts/project-links +287 -0
- package/scripts/provision-project-store +53 -0
- package/scripts/qmd-sync +139 -0
- package/scripts/rebuild-graph +244 -0
- package/scripts/select-update-target +93 -0
- package/scripts/spawn-preamble +138 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +164 -0
- package/scripts/validate-intent +54 -0
- package/scripts/versions.rb +141 -0
- package/skills/_active-intent-gate.md +1 -1
- package/skills/add-project-store/SKILL.md +54 -0
- package/skills/auto/SKILL.md +87 -7
- package/skills/auto/evals/evals.json +255 -0
- package/skills/auto/references/agent-architecture.md +155 -0
- package/skills/auto/references/agent-report-contract.md +86 -0
- package/skills/brainstorming/SKILL.md +10 -9
- package/skills/brainstorming/evals/evals.json +22 -0
- package/skills/brainstorming-grill-me/SKILL.md +6 -6
- package/skills/continuing/SKILL.md +99 -82
- package/skills/continuing/evals/evals.json +145 -0
- package/skills/continuing/references/context-management.md +32 -0
- package/skills/creating-intent/SKILL.md +82 -36
- package/skills/creating-intent/evals/evals.json +72 -0
- package/skills/creating-intent/references/lifecycle.md +81 -0
- package/skills/creating-intent/references/wikilinks.md +8 -0
- package/skills/creating-project/SKILL.md +40 -8
- package/skills/creating-project/references/hubs-projects.md +55 -0
- package/skills/dashboard/SKILL.md +126 -0
- package/skills/dashboard/evals/evals.json +22 -0
- package/skills/dashboard/templates/dashboard-global.md +31 -0
- package/skills/dashboard/templates/dashboard-project.md +40 -0
- package/skills/doctor/SKILL.md +51 -4
- package/skills/doctor/references/gates-stuck-detection.md +38 -0
- package/skills/doctor/report.md +4 -0
- package/skills/evaluating-skills/SKILL.md +140 -0
- package/skills/evaluating-skills/assets/eval-template.json +12 -0
- package/skills/evaluating-skills/evals/evals.json +75 -0
- package/skills/evaluating-skills/references/convention-checks.md +76 -0
- package/skills/evaluating-skills/references/eval-methodology.md +154 -0
- package/skills/executing-plan/SKILL.md +5 -3
- package/skills/install/SKILL.md +69 -8
- package/skills/intent-curator/SKILL.md +6 -4
- package/skills/intent-curator/evals/evals.json +22 -0
- package/skills/linking-intents/SKILL.md +22 -7
- package/skills/linking-intents/evals/evals.json +22 -0
- package/skills/linking-intents/references/zettelkasten.md +45 -0
- package/skills/managing-index/SKILL.md +11 -1
- package/skills/managing-index/evals/evals.json +22 -0
- package/skills/managing-index/references/zettelkasten-linking.md +7 -2
- package/skills/releasing/SKILL.md +80 -23
- package/skills/releasing/references/deprecations.md +60 -0
- package/skills/research/SKILL.md +10 -2
- package/skills/research/evals/evals.json +22 -0
- package/skills/savepoint/SKILL.md +46 -37
- package/skills/savepoint/references/context-management.md +32 -0
- package/skills/uninstall/SKILL.md +39 -28
- package/skills/update/SKILL.md +41 -44
- package/skills/versions/SKILL.md +65 -0
- package/skills/writing-instructions/SKILL.md +159 -0
- package/skills/writing-instructions/references/agentskills-spec.md +135 -0
- package/skills/writing-plans/SKILL.md +5 -5
- package/templates/agents.md +7 -7
- package/templates/outcome.md +13 -0
- package/templates/savepoint.md +14 -13
- package/templates/spec.md +25 -0
- package/bin/install.js +0 -29
package/hooks/gate-check
CHANGED
|
@@ -6,5 +6,7 @@ if [ -z "$FILE_PATH" ]; then
|
|
|
6
6
|
exit 0
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
|
+
SESSION_ID=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
|
|
10
|
+
|
|
9
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
-
ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH"
|
|
12
|
+
ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH" "$SESSION_ID"
|
package/hooks/hooks.json
CHANGED
|
@@ -29,6 +29,38 @@
|
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
|
+
"PreToolUse": [
|
|
33
|
+
{
|
|
34
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" code-gate",
|
|
39
|
+
"statusMessage": "Checking lifecycle gate..."
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"matcher": "Write",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" create-gate",
|
|
49
|
+
"statusMessage": "Checking create gate..."
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"matcher": "Bash",
|
|
55
|
+
"hooks": [
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" bash-gate",
|
|
59
|
+
"statusMessage": "Checking lifecycle gate..."
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
32
64
|
"PostToolUse": [
|
|
33
65
|
{
|
|
34
66
|
"matcher": "Write|Edit",
|
|
@@ -61,6 +93,26 @@
|
|
|
61
93
|
"statusMessage": "Checking future intents..."
|
|
62
94
|
}
|
|
63
95
|
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"matcher": "",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "command",
|
|
102
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/auto-arm\"",
|
|
103
|
+
"statusMessage": "Checking auto mode..."
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"matcher": "",
|
|
109
|
+
"hooks": [
|
|
110
|
+
{
|
|
111
|
+
"type": "command",
|
|
112
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" qmd-search",
|
|
113
|
+
"statusMessage": "Searching QMD..."
|
|
114
|
+
}
|
|
115
|
+
]
|
|
64
116
|
}
|
|
65
117
|
]
|
|
66
118
|
}
|
package/hooks/qmd-search
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# qmd-first search hook launcher (intent 66). No-op when there is no global store.
|
|
3
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
4
|
+
if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
5
|
+
exit 0
|
|
6
|
+
fi
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
exec ruby "$SCRIPT_DIR/../scripts/hook-qmd-search" "$HOME/.plastic"
|
package/hooks/statusline
CHANGED
|
@@ -1,56 +1,165 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# plastic-hook-version:
|
|
3
|
-
# StatusLine hook
|
|
2
|
+
# plastic-hook-version: 3.0.0
|
|
3
|
+
# StatusLine hook - Plastic owns the full statusline (no chaining).
|
|
4
|
+
# Renders: {model} - Plastic {version}[ -> {next}] - {path} - [{project}: {id} {title}]
|
|
5
|
+
# Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads only.
|
|
4
6
|
|
|
5
7
|
INPUT=$(cat)
|
|
6
8
|
|
|
7
|
-
# ---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
# --- Colors (ANSI-C quoting; bash 3.2 supports $'\033') ---
|
|
10
|
+
C_MODEL=$'\033[38;2;90;140;220m' # navy blue - model (lightened for legibility)
|
|
11
|
+
C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
|
|
12
|
+
C_NEXT=$'\033[33m' # yellow - next version
|
|
13
|
+
C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
|
|
14
|
+
C_BRACK=$'\033[33m' # yellow - work-unit brackets
|
|
15
|
+
C_PROJ=$'\033[38;2;249;140;30m' # orange - project name
|
|
16
|
+
C_ID=$'\033[38;2;45;212;191m' # teal - intent id (accent between orange + yellow)
|
|
17
|
+
C_INTENT=$'\033[33m' # yellow - intent title
|
|
18
|
+
C_SEP=$'\033[38;2;128;134;144m' # gray - separators
|
|
19
|
+
RST=$'\033[0m'
|
|
20
|
+
SEP="${C_SEP} "$'\302\267'" ${RST}" # " . " middot, dim
|
|
21
|
+
|
|
22
|
+
# --- Extract a flat JSON string field from stdin: json_str <key> ---
|
|
23
|
+
json_str() {
|
|
24
|
+
printf '%s' "$INPUT" \
|
|
25
|
+
| grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
|
|
26
|
+
| head -1 \
|
|
27
|
+
| sed "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"//;s/\"$//"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# --- Model (model.display_name) ---
|
|
31
|
+
MODEL=$(json_str "display_name")
|
|
32
|
+
|
|
33
|
+
# --- Path (workspace.current_dir, fallback top-level cwd) ---
|
|
34
|
+
REAL_CWD=$(json_str "current_dir")
|
|
35
|
+
[ -z "$REAL_CWD" ] && REAL_CWD=$(json_str "cwd")
|
|
36
|
+
# Abbreviate $HOME -> ~
|
|
37
|
+
CWD="$REAL_CWD"
|
|
38
|
+
case "$CWD" in
|
|
39
|
+
"$HOME") CWD="~" ;;
|
|
40
|
+
"$HOME"/*) CWD="~${CWD#"$HOME"}" ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# --- Plastic version ---
|
|
44
|
+
VERSION=""
|
|
45
|
+
[ -f "$HOME/.plastic/VERSION" ] && VERSION=$(tr -d ' \t\n' < "$HOME/.plastic/VERSION")
|
|
46
|
+
|
|
47
|
+
# --- Update (next version when one is available) ---
|
|
48
|
+
NEXT=""
|
|
49
|
+
CACHE_FILE="$HOME/.plastic/.cache/update-check.json"
|
|
50
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
51
|
+
if grep -q '"updateAvailable"[[:space:]]*:[[:space:]]*true' "$CACHE_FILE" 2>/dev/null; then
|
|
52
|
+
LATEST=$(grep -o '"latest"[[:space:]]*:[[:space:]]*"[^"]*"' "$CACHE_FILE" 2>/dev/null \
|
|
53
|
+
| head -1 | sed 's/.*"latest"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
54
|
+
if [ -n "$LATEST" ]; then
|
|
55
|
+
case "$LATEST" in
|
|
56
|
+
*-*) NEXT="${LATEST##*-}" ;; # 1.0.0-beta.3 -> beta.3
|
|
57
|
+
*) NEXT="$LATEST" ;;
|
|
58
|
+
esac
|
|
59
|
+
fi
|
|
17
60
|
fi
|
|
18
61
|
fi
|
|
19
62
|
|
|
20
|
-
# ---
|
|
21
|
-
|
|
63
|
+
# --- Scope resolution: cwd -> registered project store, else global ---
|
|
64
|
+
# Match the longest registered project path that prefixes the real cwd.
|
|
65
|
+
PROJECTS_YML="$HOME/.plastic/projects.yml"
|
|
66
|
+
SLUG=""
|
|
67
|
+
if [ -n "$REAL_CWD" ] && [ -f "$PROJECTS_YML" ]; then
|
|
68
|
+
SLUG=$(awk -v cwd="$REAL_CWD" '
|
|
69
|
+
/^ [A-Za-z0-9_.-]+:[[:space:]]*$/ { s=$1; sub(/:$/,"",s); next }
|
|
70
|
+
/^ path:/ {
|
|
71
|
+
p=$0; sub(/^[[:space:]]*path:[[:space:]]*/,"",p); gsub(/"/,"",p);
|
|
72
|
+
if (length(p)>0 && (cwd==p || index(cwd, p"/")==1) && length(p)>best) { best=length(p); win=s }
|
|
73
|
+
}
|
|
74
|
+
END { if (win!="") print win }
|
|
75
|
+
' "$PROJECTS_YML")
|
|
76
|
+
fi
|
|
22
77
|
|
|
23
|
-
|
|
24
|
-
INDEX="$HOME/.plastic/INDEX.md"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
PLASTIC_PARTS="plastic: ${INTENT}"
|
|
30
|
-
fi
|
|
78
|
+
if [ -n "$SLUG" ]; then
|
|
79
|
+
INDEX="$HOME/.plastic/projects/$SLUG/INDEX.md"
|
|
80
|
+
PROJ_LABEL="$SLUG"
|
|
81
|
+
else
|
|
82
|
+
INDEX="$HOME/.plastic/INDEX.md"
|
|
83
|
+
PROJ_LABEL="global"
|
|
31
84
|
fi
|
|
85
|
+
INDEX_DIR=$(dirname "$INDEX")
|
|
32
86
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
87
|
+
# --- Current-session work-unit: the live bridge wins over savepoint recency ---
|
|
88
|
+
# The statusline receives the real session_id on stdin; the session's bridge
|
|
89
|
+
# (plastic-{session_id}.json) is the authoritative "what THIS session is driving".
|
|
90
|
+
# This beats the shared, savepoint-recency heuristic so parallel sessions on one
|
|
91
|
+
# store no longer overwrite each other's line. grep/sed only - no jq, no ruby.
|
|
92
|
+
WORK=""
|
|
93
|
+
SID=$(json_str "session_id")
|
|
94
|
+
if [ -n "$SID" ]; then
|
|
95
|
+
BRIDGE="${PLASTIC_TMP:-/tmp}/plastic-${SID}.json"
|
|
96
|
+
if [ -f "$BRIDGE" ]; then
|
|
97
|
+
# Bridge JSON is pretty-printed (one field per line). Scope extraction to the
|
|
98
|
+
# "intent" object so a sibling top-level "id"/"name" can never win (the intent
|
|
99
|
+
# object holds only string fields, so the first "}" closes it). grep/sed only.
|
|
100
|
+
INTENT_BLOCK=$(sed -n '/"intent"[[:space:]]*:[[:space:]]*{/,/}/p' "$BRIDGE" 2>/dev/null)
|
|
101
|
+
B_ID=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
102
|
+
| sed 's/.*"id"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
103
|
+
B_NAME=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
104
|
+
| sed 's/.*"name"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
105
|
+
if [ -n "$B_ID" ] && [ -n "$B_NAME" ]; then
|
|
106
|
+
EMDASH=$'\342\200\224'
|
|
107
|
+
WORK="${B_ID} ${EMDASH} ${B_NAME}"
|
|
45
108
|
fi
|
|
46
109
|
fi
|
|
47
110
|
fi
|
|
48
111
|
|
|
49
|
-
# ---
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
112
|
+
# --- Most-recently-accessed active intent (mirror dashboard: last savepoint ts) ---
|
|
113
|
+
# Fallback only: runs when no session bridge resolved a work-unit above.
|
|
114
|
+
if [ -z "$WORK" ] && [ -f "$INDEX" ]; then
|
|
115
|
+
BEST_TS=""
|
|
116
|
+
while IFS= read -r line; do
|
|
117
|
+
[ -z "$line" ] && continue
|
|
118
|
+
inside=${line#*[}; inside=${inside%%]*} # "{id} - {title}"
|
|
119
|
+
dir=${line#*](store/}; dir=${dir%%/*} # intent directory name
|
|
120
|
+
[ -z "$dir" ] && continue
|
|
121
|
+
sp="$INDEX_DIR/store/$dir/savepoint.md"
|
|
122
|
+
ts=""
|
|
123
|
+
if [ -f "$sp" ]; then
|
|
124
|
+
ts=$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$sp" 2>/dev/null | tail -1)
|
|
125
|
+
fi
|
|
126
|
+
# ISO8601 sorts lexicographically; keep the newest. Empty ts sorts lowest.
|
|
127
|
+
if [ -z "$BEST_TS" ] || { [ -n "$ts" ] && [ "$ts" \> "$BEST_TS" ]; }; then
|
|
128
|
+
BEST_TS="$ts"
|
|
129
|
+
WORK="$inside"
|
|
130
|
+
fi
|
|
131
|
+
done <<EOF
|
|
132
|
+
$(sed -n '/^## Active$/,/^## /{/^- \[/p;}' "$INDEX")
|
|
133
|
+
EOF
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Build the work-unit "[{project}: {id} {title}]" with title truncation ---
|
|
137
|
+
WORK_SEG=""
|
|
138
|
+
if [ -n "$WORK" ]; then
|
|
139
|
+
# Split "{id} - {title}" on the em-dash (U+2014).
|
|
140
|
+
EMDASH=$'\342\200\224'
|
|
141
|
+
id=${WORK%% $EMDASH *}
|
|
142
|
+
title=${WORK#* $EMDASH }
|
|
143
|
+
# Fallback if the em-dash form is absent: split on first space.
|
|
144
|
+
if [ "$id" = "$WORK" ]; then id=${WORK%% *}; title=${WORK#* }; fi
|
|
145
|
+
# Truncate long titles (~40 chars).
|
|
146
|
+
if [ ${#title} -gt 40 ]; then title="$(printf '%.40s' "$title")..."; fi
|
|
147
|
+
WORK_SEG="${C_BRACK}[${RST}${C_PROJ}${PROJ_LABEL}${RST}${C_INTENT}: ${RST}${C_ID}${id}${RST}${C_INTENT} ${title}${RST}${C_BRACK}]${RST}"
|
|
56
148
|
fi
|
|
149
|
+
|
|
150
|
+
# --- Assemble segments in order, joined by " . " ---
|
|
151
|
+
PARTS=""
|
|
152
|
+
add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
|
|
153
|
+
|
|
154
|
+
# Order: stable values first (model, work-unit), then the volatile-length
|
|
155
|
+
# values last (version, path) so the line does not visually jump as they change.
|
|
156
|
+
[ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
|
|
157
|
+
[ -n "$WORK_SEG" ] && add "$WORK_SEG"
|
|
158
|
+
if [ -n "$VERSION" ]; then
|
|
159
|
+
vseg="${C_VER}Plastic ${VERSION}${RST}"
|
|
160
|
+
[ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
|
|
161
|
+
add "$vseg"
|
|
162
|
+
fi
|
|
163
|
+
[ -n "$CWD" ] && add "${C_PATH}${CWD}${RST}"
|
|
164
|
+
|
|
165
|
+
printf '%s\n' "$PARTS"
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalom/plastic",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
4
4
|
"description": "Intent-driven idea development system for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"plastic": "bin/
|
|
7
|
+
"plastic": "bin/plastic.js"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# agent-report - deterministic filesystem-derived completion report (intent 74).
|
|
6
|
+
#
|
|
7
|
+
# The auto-mode report contract is decision-shaped (the spawn preamble's
|
|
8
|
+
# REPORT_CONTRACT plus the role prompts ask every dispatched agent to END with a
|
|
9
|
+
# structured completion report). Because child-agent honor is best-effort across
|
|
10
|
+
# harnesses (Tier B/C, docs/reference/harness-adapters.md), the contract is never a
|
|
11
|
+
# hard block. This script is the always-a-report fallback: when a dispatched agent
|
|
12
|
+
# returns no usable report, the enforcer runs it to synthesize one from the intent
|
|
13
|
+
# directory, so the handoff account always exists.
|
|
14
|
+
#
|
|
15
|
+
# Like scripts/spawn-preamble, it is a PURE function of the intent dir: no network,
|
|
16
|
+
# no randomness, no wall-clock reads. Two runs over the same on-disk state produce
|
|
17
|
+
# byte-identical output.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# agent-report <intent_dir> [--role ROLE]
|
|
21
|
+
#
|
|
22
|
+
# Exit codes: 0 (report emitted), 2 (usage).
|
|
23
|
+
|
|
24
|
+
require_relative "lib/bridge"
|
|
25
|
+
|
|
26
|
+
def parse_args(argv)
|
|
27
|
+
role = nil
|
|
28
|
+
positional = []
|
|
29
|
+
i = 0
|
|
30
|
+
while i < argv.length
|
|
31
|
+
case argv[i]
|
|
32
|
+
when "--role"
|
|
33
|
+
role = argv[i + 1]
|
|
34
|
+
i += 2
|
|
35
|
+
else
|
|
36
|
+
positional << argv[i]
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
[positional.first, role]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read the intent file frontmatter via the same path spawn-preamble uses, so the
|
|
44
|
+
# id/intent we report match what the rest of Plastic sees. Never raises.
|
|
45
|
+
def frontmatter_for(intent_dir)
|
|
46
|
+
ifile = Bridge.intent_file(intent_dir)
|
|
47
|
+
return {} unless File.exist?(ifile)
|
|
48
|
+
|
|
49
|
+
content = File.read(ifile)
|
|
50
|
+
return {} unless content.start_with?("---")
|
|
51
|
+
|
|
52
|
+
parts = content.split("---", 3)
|
|
53
|
+
return {} if parts.length < 3
|
|
54
|
+
|
|
55
|
+
require "yaml"
|
|
56
|
+
require "date"
|
|
57
|
+
require "time"
|
|
58
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}
|
|
59
|
+
rescue StandardError
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Current stage: prefer the last non-empty savepoint line (the ledger encodes the
|
|
64
|
+
# furthest-reached milestone), else the file-derived stage. Same logic as
|
|
65
|
+
# spawn-preamble's current_stage.
|
|
66
|
+
STAGE_LABELS = {
|
|
67
|
+
"what" => "What", "why" => "Why", "how" => "How",
|
|
68
|
+
"exec" => "Exec", "done" => "Done"
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
def current_stage(intent_dir)
|
|
72
|
+
ledger = File.join(intent_dir, Bridge::SAVEPOINT_FILE)
|
|
73
|
+
if File.exist?(ledger)
|
|
74
|
+
last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
|
|
75
|
+
return last if last
|
|
76
|
+
end
|
|
77
|
+
STAGE_LABELS.fetch(Bridge.derive_stage(intent_dir), Bridge.derive_stage(intent_dir))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# checklist.md checked/total: count GFM task-list items. Returns "n/a" when there
|
|
81
|
+
# is no checklist (a checklist is only meaningful from How onward).
|
|
82
|
+
def checklist_progress(intent_dir)
|
|
83
|
+
path = File.join(intent_dir, "checklist.md")
|
|
84
|
+
return "n/a" unless Bridge.stage_file_present?(path)
|
|
85
|
+
|
|
86
|
+
lines = File.readlines(path)
|
|
87
|
+
total = lines.count { |l| l =~ /^\s*- \[[ xX]\]/ }
|
|
88
|
+
checked = lines.count { |l| l =~ /^\s*- \[[xX]\]/ }
|
|
89
|
+
return "n/a" if total.zero?
|
|
90
|
+
|
|
91
|
+
"#{checked}/#{total}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The outcome line: first content line under outcome.md's "## Summary" (or the first
|
|
95
|
+
# non-heading content line), else a placeholder. Pure read of the intent dir.
|
|
96
|
+
def outcome_line(intent_dir)
|
|
97
|
+
path = File.join(intent_dir, "outcome.md")
|
|
98
|
+
return "(no outcome yet)" unless Bridge.stage_file_present?(path)
|
|
99
|
+
|
|
100
|
+
lines = File.readlines(path).map(&:rstrip)
|
|
101
|
+
start = lines.index { |l| l.strip.downcase == "## summary" }
|
|
102
|
+
scan = start ? lines[(start + 1)..] : lines
|
|
103
|
+
(scan || []).each do |l|
|
|
104
|
+
s = l.strip
|
|
105
|
+
next if s.empty? || s.start_with?("#") || s.start_with?("<!--")
|
|
106
|
+
return s
|
|
107
|
+
end
|
|
108
|
+
"(no outcome yet)"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
intent_dir_arg, role = parse_args(ARGV)
|
|
112
|
+
|
|
113
|
+
if intent_dir_arg.nil? || intent_dir_arg.empty?
|
|
114
|
+
warn "usage: agent-report <intent_dir> [--role ROLE]"
|
|
115
|
+
exit 2
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
119
|
+
|
|
120
|
+
fm = frontmatter_for(intent_dir)
|
|
121
|
+
id = fm["id"].to_s.strip
|
|
122
|
+
intent_name = fm["intent"].to_s.strip
|
|
123
|
+
id = "(unknown)" if id.empty?
|
|
124
|
+
intent_name = "(unknown)" if intent_name.empty?
|
|
125
|
+
|
|
126
|
+
stage = current_stage(intent_dir)
|
|
127
|
+
role_label = (role && !role.empty?) ? role : stage
|
|
128
|
+
artifacts = Bridge.has_files(intent_dir)
|
|
129
|
+
artifacts_str = artifacts.empty? ? "(none)" : artifacts.join(", ")
|
|
130
|
+
|
|
131
|
+
lines = []
|
|
132
|
+
lines << "=== Plastic agent report (synthesized) ==="
|
|
133
|
+
lines << "Intent: #{id} - #{intent_name}"
|
|
134
|
+
lines << "Stage: #{stage}"
|
|
135
|
+
lines << "Role: #{role_label}"
|
|
136
|
+
lines << "Status: synthesized (filesystem-derived; no agent-authored report)"
|
|
137
|
+
lines << "Artifacts present: #{artifacts_str}"
|
|
138
|
+
lines << "Checklist: #{checklist_progress(intent_dir)}"
|
|
139
|
+
lines << "Outcome: #{outcome_line(intent_dir)}"
|
|
140
|
+
lines << "=== end report ==="
|
|
141
|
+
|
|
142
|
+
puts lines.join("\n")
|