hermes-task-framework 1.0.0__py3-none-any.whl
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.
- hermes_task_framework-1.0.0.dist-info/METADATA +7 -0
- hermes_task_framework-1.0.0.dist-info/RECORD +56 -0
- hermes_task_framework-1.0.0.dist-info/WHEEL +5 -0
- hermes_task_framework-1.0.0.dist-info/licenses/LICENSE +1 -0
- hermes_task_framework-1.0.0.dist-info/top_level.txt +1 -0
- task-framework/__init__.py +2 -0
- task-framework/skills/task-external-repos-pattern/SKILL.md +165 -0
- task-framework/skills/task-framework/CHANGELOG.md +45 -0
- task-framework/skills/task-framework/SKILL.md +1365 -0
- task-framework/skills/task-framework/docs/TASKS.md +3 -0
- task-framework/skills/task-framework/docs/architecture.dot +65 -0
- task-framework/skills/task-framework/docs/architecture.svg +188 -0
- task-framework/skills/task-framework/docs/product-requirements.md +65 -0
- task-framework/skills/task-framework/references/auto-runner.md +32 -0
- task-framework/skills/task-framework/references/composites/code-review-session.md +30 -0
- task-framework/skills/task-framework/references/composites/research.md +45 -0
- task-framework/skills/task-framework/references/composites/software-dev.md +41 -0
- task-framework/skills/task-framework/references/document-analysis-workflow.md +95 -0
- task-framework/skills/task-framework/references/file-safety-lesson.md +38 -0
- task-framework/skills/task-framework/references/generating-output-documents.md +166 -0
- task-framework/skills/task-framework/references/operations/code-write.md +30 -0
- task-framework/skills/task-framework/references/operations/document-write.md +185 -0
- task-framework/skills/task-framework/references/operations/info-search.md +27 -0
- task-framework/skills/task-framework/references/operations/web-research.md +57 -0
- task-framework/skills/task-framework/references/policy-time-metadata.md +137 -0
- task-framework/skills/task-framework/references/research-workflow.md +220 -0
- task-framework/skills/task-framework/references/task-format-validation.md +25 -0
- task-framework/skills/task-framework/references/task-hash-naming.md +40 -0
- task-framework/skills/task-framework/references/task-lifecycle-example.md +122 -0
- task-framework/skills/task-framework/references/task-overview-discovery.md +31 -0
- task-framework/skills/task-framework/references/task-types/analysis.md +60 -0
- task-framework/skills/task-framework/references/task-types/external-audit.md +56 -0
- task-framework/skills/task-framework/references/task-types/video-production-pipeline.md +77 -0
- task-framework/skills/task-framework/scripts/__init__.py +0 -0
- task-framework/skills/task-framework/scripts/__pycache__/manage_task.cpython-312.pyc +0 -0
- task-framework/skills/task-framework/scripts/__pycache__/task_ref.cpython-312.pyc +0 -0
- task-framework/skills/task-framework/scripts/__pycache__/update-index.cpython-311.pyc +0 -0
- task-framework/skills/task-framework/scripts/__pycache__/update-index.cpython-312.pyc +0 -0
- task-framework/skills/task-framework/scripts/convert_md_to_pdf.py +236 -0
- task-framework/skills/task-framework/scripts/manage_task.py +442 -0
- task-framework/skills/task-framework/scripts/task-runner.sh +42 -0
- task-framework/skills/task-framework/scripts/task_ref.py +119 -0
- task-framework/skills/task-framework/scripts/update-index.py +293 -0
- task-framework/skills/task-framework/templates/TASK.md +114 -0
- task-framework/skills/task-framework/templates/TASK_MEMORY.md +27 -0
- task-framework/skills/task-framework/templates/run.py +187 -0
- task-framework/skills/task-lifecycle-edge-cases/SKILL.md +84 -0
- task-framework/skills/task-lifecycle-edge-cases/references/task-5d5a1a-recovery-example.md +67 -0
- task-framework/skills/task-lifecycle-portability/SKILL.md +128 -0
- task-framework/skills/task-lifecycle-portability/references/design-session-20260611.md +32 -0
- task-framework/skills/task-lifecycle-portability/references/output-model-design.md +55 -0
- task-framework/skills/task-lifecycle-portability/references/pipeline-output-transition.md +41 -0
- task-framework/skills/task-lifecycle-portability/references/task-recovery-5d5a1a-example.md +27 -0
- task-framework/skills/task-lifecycle-portability/references/task-recovery-procedure.md +63 -0
- task-framework/skills/task-timestamp-convention/SKILL.md +72 -0
- task-framework/skills/task-tracker/SKILL.md +80 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Task Runner — execute a command with timestamped stdout/stderr logging
|
|
3
|
+
# Usage: task-runner.sh <task-dir> <command...>
|
|
4
|
+
#
|
|
5
|
+
# Creates:
|
|
6
|
+
# <task-dir>/logs/output.YYYYMMDD-HHMMSS.log
|
|
7
|
+
# <task-dir>/logs/error.YYYYMMDD-HHMMSS.log
|
|
8
|
+
# stdout and stderr also go to terminal.
|
|
9
|
+
# Exit code preserved.
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
if [ $# -lt 2 ]; then
|
|
14
|
+
echo "Usage: $0 <task-dir> <command...>" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
TASK_DIR="$1"
|
|
19
|
+
shift
|
|
20
|
+
|
|
21
|
+
mkdir -p "$TASK_DIR/logs"
|
|
22
|
+
|
|
23
|
+
TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
|
|
24
|
+
OUTPUT_LOG="$TASK_DIR/logs/output.${TIMESTAMP}.log"
|
|
25
|
+
ERROR_LOG="$TASK_DIR/logs/error.${TIMESTAMP}.log"
|
|
26
|
+
|
|
27
|
+
# Header
|
|
28
|
+
echo "-------- $(date '+%Y-%m-%d %H:%M:%S') --------" > "$OUTPUT_LOG"
|
|
29
|
+
echo "-------- $(date '+%Y-%m-%d %H:%M:%S') --------" > "$ERROR_LOG"
|
|
30
|
+
echo "Command: $*" >> "$OUTPUT_LOG"
|
|
31
|
+
echo "Command: $*" >> "$ERROR_LOG"
|
|
32
|
+
echo "--------" >> "$OUTPUT_LOG"
|
|
33
|
+
echo "--------" >> "$ERROR_LOG"
|
|
34
|
+
|
|
35
|
+
# Run command with split tee
|
|
36
|
+
# stdout → both terminal and output log
|
|
37
|
+
# stderr → both terminal and error log
|
|
38
|
+
"$@" 2> >(tee "$ERROR_LOG" >&2) | tee "$OUTPUT_LOG"
|
|
39
|
+
|
|
40
|
+
EXIT_CODE=$?
|
|
41
|
+
echo "Exit code: $EXIT_CODE" | tee -a "$OUTPUT_LOG" | tee -a "$ERROR_LOG" >/dev/null
|
|
42
|
+
exit $EXIT_CODE
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Task reference resolution utilities.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from task_ref import resolve_ref, check_cycles, create_task_meta
|
|
5
|
+
"""
|
|
6
|
+
import os, glob, json, hashlib, random, datetime, re, sys, subprocess
|
|
7
|
+
|
|
8
|
+
TASKS_ROOT = os.path.expanduser(os.environ.get("HERMES_TASKS_ROOT", "~/studio/hermes/tasks"))
|
|
9
|
+
|
|
10
|
+
def _hash6():
|
|
11
|
+
return hashlib.md5(str(random.random()).encode()).hexdigest()[:6]
|
|
12
|
+
|
|
13
|
+
def resolve_ref(ref_str):
|
|
14
|
+
"""Resolve ref:hash/output_name to absolute path."""
|
|
15
|
+
if not ref_str.startswith('ref:'):
|
|
16
|
+
return ref_str
|
|
17
|
+
parts = ref_str[4:].split('/', 1)
|
|
18
|
+
hash_id = parts[0]
|
|
19
|
+
output_name = parts[1] if len(parts) > 1 else None
|
|
20
|
+
|
|
21
|
+
matches = glob.glob(os.path.join(TASKS_ROOT, f'*{hash_id}*'))
|
|
22
|
+
if not matches:
|
|
23
|
+
raise FileNotFoundError(f'Task with hash {hash_id} not found in {TASKS_ROOT}')
|
|
24
|
+
|
|
25
|
+
task_dir = matches[0]
|
|
26
|
+
meta_path = os.path.join(task_dir, '.hermes-task.json')
|
|
27
|
+
if not os.path.exists(meta_path):
|
|
28
|
+
raise FileNotFoundError(f'.hermes-task.json not found in {task_dir}')
|
|
29
|
+
|
|
30
|
+
with open(meta_path) as f:
|
|
31
|
+
meta = json.load(f)
|
|
32
|
+
|
|
33
|
+
if output_name:
|
|
34
|
+
if output_name not in meta.get('outputs', {}):
|
|
35
|
+
raise KeyError(f'Output "{output_name}" not declared in {task_dir}/.hermes-task.json. '
|
|
36
|
+
f'Available: {list(meta.get("outputs", {}).keys())}')
|
|
37
|
+
return os.path.join(task_dir, meta['outputs'][output_name])
|
|
38
|
+
|
|
39
|
+
return task_dir
|
|
40
|
+
|
|
41
|
+
def check_cycles(meta):
|
|
42
|
+
"""Walk dependency graph, raise ValueError on cycles."""
|
|
43
|
+
seen = [meta['hash']]
|
|
44
|
+
stack = list(meta.get('dependencies', []))
|
|
45
|
+
while stack:
|
|
46
|
+
h = stack.pop()
|
|
47
|
+
if h in seen:
|
|
48
|
+
raise ValueError(f'Cycle detected: {h} already in path {" → ".join(seen)}')
|
|
49
|
+
seen.append(h)
|
|
50
|
+
matches = glob.glob(os.path.join(TASKS_ROOT, f'*{h}*'))
|
|
51
|
+
if matches:
|
|
52
|
+
dep_meta_path = os.path.join(matches[0], '.hermes-task.json')
|
|
53
|
+
if os.path.exists(dep_meta_path):
|
|
54
|
+
dep_meta = json.load(open(dep_meta_path))
|
|
55
|
+
stack.extend(dep_meta.get('dependencies', []))
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def create_task_meta(task_dir, name, dependencies=None, related=None, supersedes=None, board="default"):
|
|
59
|
+
"""Create .hermes-task.json in a task directory. Returns the hash.
|
|
60
|
+
Also creates a kanban card on the specified board and stores the card ID."""
|
|
61
|
+
h = _hash6()
|
|
62
|
+
meta = {
|
|
63
|
+
"hash": h,
|
|
64
|
+
"name": name,
|
|
65
|
+
"created_at": datetime.datetime.now().isoformat(),
|
|
66
|
+
"outputs": {},
|
|
67
|
+
"dependencies": dependencies or [],
|
|
68
|
+
"related": related or [],
|
|
69
|
+
"supersedes": supersedes or [],
|
|
70
|
+
"kanban_card_id": "",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Check cycles before writing
|
|
74
|
+
if meta['dependencies']:
|
|
75
|
+
check_cycles(meta)
|
|
76
|
+
|
|
77
|
+
meta_path = os.path.join(task_dir, '.hermes-task.json')
|
|
78
|
+
with open(meta_path, 'w') as f:
|
|
79
|
+
json.dump(meta, f, indent=2, ensure_ascii=False)
|
|
80
|
+
|
|
81
|
+
# Create kanban card
|
|
82
|
+
dirname = os.path.basename(task_dir)
|
|
83
|
+
title = f"{name} [task:{dirname}]"
|
|
84
|
+
env = {**os.environ}
|
|
85
|
+
env.pop("HERMES_KANBAN_BOARD", None) # Avoid env leak override
|
|
86
|
+
try:
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["hermes", "kanban", "boards", "switch", board],
|
|
89
|
+
capture_output=True, text=True, timeout=10, env=env,
|
|
90
|
+
)
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
["hermes", "kanban", "create", title, "--assignee", "default"],
|
|
93
|
+
capture_output=True, text=True, timeout=15, env=env,
|
|
94
|
+
)
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
# Parse card ID from output: "Created t_xxx (ready, ...)"
|
|
97
|
+
out = result.stdout.strip()
|
|
98
|
+
m = re.search(r'Created (t_[a-f0-9]+)', out)
|
|
99
|
+
if m:
|
|
100
|
+
card_id = m.group(1)
|
|
101
|
+
meta["kanban_card_id"] = card_id
|
|
102
|
+
# Update .hermes-task.json with card id
|
|
103
|
+
with open(meta_path, 'w') as f:
|
|
104
|
+
json.dump(meta, f, indent=2, ensure_ascii=False)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f" ⚠ kanban card creation failed: {e}", file=sys.stderr)
|
|
107
|
+
|
|
108
|
+
return h
|
|
109
|
+
|
|
110
|
+
def set_output(task_dir, name, path):
|
|
111
|
+
"""Register a named output in .hermes-task.json."""
|
|
112
|
+
meta_path = os.path.join(task_dir, '.hermes-task.json')
|
|
113
|
+
if not os.path.exists(meta_path):
|
|
114
|
+
raise FileNotFoundError(f'No .hermes-task.json in {task_dir}')
|
|
115
|
+
with open(meta_path) as f:
|
|
116
|
+
meta = json.load(f)
|
|
117
|
+
meta['outputs'][name] = path
|
|
118
|
+
with open(meta_path, 'w') as f:
|
|
119
|
+
json.dump(meta, f, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
update-index.py — Update tasks/ root index files.
|
|
4
|
+
|
|
5
|
+
Regenerates both:
|
|
6
|
+
1. tasks/README.md — summary table (directory façade)
|
|
7
|
+
2. tasks/TASKS.md — aggregated checklist view (deep overview)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/update-index.py # default: ~/studio/hermes/tasks
|
|
11
|
+
python3 scripts/update-index.py --tasks-dir /path # custom path
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os, sys, re, glob
|
|
15
|
+
|
|
16
|
+
TASKS_ROOT = os.path.expanduser(os.environ.get("HERMES_TASKS_ROOT", "~/studio/hermes/tasks"))
|
|
17
|
+
|
|
18
|
+
def gather_tasks(tasks_root):
|
|
19
|
+
"""Return sorted list of (dirname, ts, name, title, status, desc, checklist, notes, related_tasks, related_tickets, cluster_info)."""
|
|
20
|
+
task_dirs = sorted(glob.glob(os.path.join(tasks_root, "2*/")))
|
|
21
|
+
results = []
|
|
22
|
+
for d in task_dirs:
|
|
23
|
+
dirname = os.path.basename(d.rstrip('/'))
|
|
24
|
+
parts = dirname.split('.', 1)
|
|
25
|
+
ts = parts[0]
|
|
26
|
+
name = parts[1] if len(parts) > 1 else dirname
|
|
27
|
+
|
|
28
|
+
task_md_path = os.path.join(d, "TASK.md")
|
|
29
|
+
title = name
|
|
30
|
+
status = "—"
|
|
31
|
+
desc = ""
|
|
32
|
+
checklist = []
|
|
33
|
+
notes = []
|
|
34
|
+
related_tasks = []
|
|
35
|
+
related_tickets = []
|
|
36
|
+
cluster_info = {"affinity": "", "claimed_by": ""}
|
|
37
|
+
|
|
38
|
+
# Read .hermes-task.json for cluster fields
|
|
39
|
+
meta_path = os.path.join(d, ".hermes-task.json")
|
|
40
|
+
if os.path.exists(meta_path):
|
|
41
|
+
try:
|
|
42
|
+
import json
|
|
43
|
+
with open(meta_path) as f:
|
|
44
|
+
meta = json.load(f)
|
|
45
|
+
cluster_info["affinity"] = meta.get("affinity", "")
|
|
46
|
+
cluster_info["claimed_by"] = meta.get("claimed_by", "") or ""
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
if os.path.exists(task_md_path):
|
|
51
|
+
content = open(task_md_path, encoding='utf-8').read()
|
|
52
|
+
|
|
53
|
+
# Title: # Task: <title> or first # heading
|
|
54
|
+
m = re.search(r'^# Task:\s*(.+)$', content, re.MULTILINE)
|
|
55
|
+
if m:
|
|
56
|
+
title = m.group(1).strip()
|
|
57
|
+
else:
|
|
58
|
+
m = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
|
|
59
|
+
if m:
|
|
60
|
+
title = m.group(1).strip()
|
|
61
|
+
|
|
62
|
+
# Status: ## Status or ## 状态
|
|
63
|
+
m = re.search(r'^## (?:Status|状态)\s*\n\s*(.+?)\s*$', content, re.MULTILINE)
|
|
64
|
+
if m:
|
|
65
|
+
status = m.group(1).strip()
|
|
66
|
+
|
|
67
|
+
# Goal / Description: ## Goal or ## 目标 or ## 概述
|
|
68
|
+
m = re.search(r'^## (?:Goal|目标|概述)\s*\n\s*(.+?)\s*$', content, re.MULTILINE)
|
|
69
|
+
if m:
|
|
70
|
+
desc = m.group(1).strip()
|
|
71
|
+
|
|
72
|
+
# Checklist: ## Checklist or ## 步骤 or ## Checklist(步骤)
|
|
73
|
+
in_cl = False
|
|
74
|
+
for line in content.split('\n'):
|
|
75
|
+
if line.startswith('## Checklist') or line.startswith('## 步骤'):
|
|
76
|
+
in_cl = True
|
|
77
|
+
continue
|
|
78
|
+
if in_cl:
|
|
79
|
+
if line.startswith('## '):
|
|
80
|
+
break
|
|
81
|
+
if line.strip().startswith('- ['):
|
|
82
|
+
checklist.append(line.strip())
|
|
83
|
+
|
|
84
|
+
# Notes: ## Notes or ## 备注
|
|
85
|
+
in_nt = False
|
|
86
|
+
for line in content.split('\n'):
|
|
87
|
+
if line.startswith('## Notes') or line.startswith('## 备注'):
|
|
88
|
+
in_nt = True
|
|
89
|
+
continue
|
|
90
|
+
if in_nt and line.startswith('## '):
|
|
91
|
+
break
|
|
92
|
+
if in_nt and line.strip():
|
|
93
|
+
notes.append(line.strip())
|
|
94
|
+
|
|
95
|
+
# Related Tasks: ## Related Tasks table
|
|
96
|
+
in_rt = False
|
|
97
|
+
header_skipped = False
|
|
98
|
+
for line in content.split('\n'):
|
|
99
|
+
if line.startswith('## Related Tasks'):
|
|
100
|
+
in_rt = True
|
|
101
|
+
header_skipped = False
|
|
102
|
+
continue
|
|
103
|
+
if in_rt:
|
|
104
|
+
if line.startswith('## '):
|
|
105
|
+
break
|
|
106
|
+
if not line.strip():
|
|
107
|
+
continue
|
|
108
|
+
# Skip header row and separator row
|
|
109
|
+
striped = line.strip()
|
|
110
|
+
if striped.startswith('|') and '---' in striped:
|
|
111
|
+
header_skipped = True
|
|
112
|
+
continue
|
|
113
|
+
if striped.startswith('|') and header_skipped:
|
|
114
|
+
# Parse table row: | type | task_path | description |
|
|
115
|
+
cols = [c.strip() for c in striped.strip('|').split('|')]
|
|
116
|
+
if len(cols) >= 3:
|
|
117
|
+
rtype = cols[0].strip()
|
|
118
|
+
task_path = cols[1].strip().strip('`')
|
|
119
|
+
rdesc = cols[2].strip()
|
|
120
|
+
related_tasks.append({
|
|
121
|
+
'type': rtype,
|
|
122
|
+
'task_path': task_path,
|
|
123
|
+
'description': rdesc
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Related Tickets: ## Related Tickets table
|
|
127
|
+
in_rti = False
|
|
128
|
+
header_skipped_rti = False
|
|
129
|
+
for line in content.split('\n'):
|
|
130
|
+
if line.startswith('## Related Tickets'):
|
|
131
|
+
in_rti = True
|
|
132
|
+
header_skipped_rti = False
|
|
133
|
+
continue
|
|
134
|
+
if in_rti:
|
|
135
|
+
if line.startswith('## '):
|
|
136
|
+
break
|
|
137
|
+
if not line.strip():
|
|
138
|
+
continue
|
|
139
|
+
striped = line.strip()
|
|
140
|
+
if striped.startswith('|') and '---' in striped:
|
|
141
|
+
header_skipped_rti = True
|
|
142
|
+
continue
|
|
143
|
+
if striped.startswith('|') and header_skipped_rti:
|
|
144
|
+
cols = [c.strip() for c in striped.strip('|').split('|')]
|
|
145
|
+
if len(cols) >= 2:
|
|
146
|
+
ticket_id = cols[0].strip().strip('`').lstrip('#')
|
|
147
|
+
rdesc_ti = cols[1].strip()
|
|
148
|
+
related_tickets.append({
|
|
149
|
+
'ticket_id': ticket_id,
|
|
150
|
+
'description': rdesc_ti
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
results.append((dirname, ts, name, title, status, desc, checklist, notes, related_tasks, related_tickets, cluster_info))
|
|
154
|
+
return results
|
|
155
|
+
|
|
156
|
+
def gen_readme(tasks):
|
|
157
|
+
"""Generate README.md — summary table with ticket links."""
|
|
158
|
+
lines = ["# Tasks", "",
|
|
159
|
+
"Project tasks managed under `tasks/`. Each task is a self-contained unit of work.",
|
|
160
|
+
"",
|
|
161
|
+
"| Timestamp | Name | Status | Ticket | Description |",
|
|
162
|
+
"|---|---|---|---|---|"]
|
|
163
|
+
for dirname, ts, name, title, status, desc, _, _, _, related_tickets, cluster_info in tasks:
|
|
164
|
+
s = status.replace('\n', ' ').strip()
|
|
165
|
+
d = desc.replace('\n', ' ').strip()[:60]
|
|
166
|
+
if not d:
|
|
167
|
+
d = title[:60]
|
|
168
|
+
ticket_tag = ""
|
|
169
|
+
if related_tickets:
|
|
170
|
+
ids = ", ".join(f"#{t['ticket_id']}" for t in related_tickets)
|
|
171
|
+
ticket_tag = f"{ids}"
|
|
172
|
+
cl_tag = ""
|
|
173
|
+
if cluster_info.get("affinity") and cluster_info["affinity"] != "any":
|
|
174
|
+
cl_tag = f"[{cluster_info['affinity']}]"
|
|
175
|
+
if cluster_info.get("claimed_by"):
|
|
176
|
+
cl_tag += f" →{cluster_info['claimed_by']}"
|
|
177
|
+
lines.append(f"| {ts} | {name} | {s} {cl_tag} | {ticket_tag} | {d} |")
|
|
178
|
+
|
|
179
|
+
# Inbox
|
|
180
|
+
inbox_dir = os.path.join(TASKS_ROOT, "inbox")
|
|
181
|
+
inbox_items = []
|
|
182
|
+
if os.path.isdir(inbox_dir):
|
|
183
|
+
inbox_items = sorted(os.listdir(inbox_dir))
|
|
184
|
+
if inbox_items:
|
|
185
|
+
lines.extend(["", "### Inbox"] + [f"- {item}" for item in inbox_items])
|
|
186
|
+
|
|
187
|
+
lines.append("")
|
|
188
|
+
return '\n'.join(lines)
|
|
189
|
+
|
|
190
|
+
def gen_tasks_md(tasks):
|
|
191
|
+
"""Generate TASKS.md — aggregated checklist view with relationship display."""
|
|
192
|
+
lines = ["# Tasks 聚合索引", "",
|
|
193
|
+
"所有任务的状态、Checklist 和关联关系汇总。由 `scripts/update-index.py` 自动生成,无需手动编辑。",
|
|
194
|
+
"", "---", ""]
|
|
195
|
+
for dirname, ts, name, title, status, desc, checklist, notes, related_tasks, related_tickets, cluster_info in tasks:
|
|
196
|
+
lines.append(f"## [{name}]({dirname}/)")
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append(f"**状态:** {status.replace(chr(10), ' ').strip()}")
|
|
199
|
+
if cluster_info.get("affinity"):
|
|
200
|
+
cl_line = f"**Cluster:** affinity={cluster_info['affinity']}"
|
|
201
|
+
if cluster_info.get("claimed_by"):
|
|
202
|
+
cl_line += f", claimed_by={cluster_info['claimed_by']}"
|
|
203
|
+
lines.append(cl_line)
|
|
204
|
+
lines.append("")
|
|
205
|
+
if desc:
|
|
206
|
+
lines.append("")
|
|
207
|
+
lines.append(f"**目标:** {desc}")
|
|
208
|
+
lines.append("")
|
|
209
|
+
lines.append(f"**目录:** `{dirname}/`")
|
|
210
|
+
|
|
211
|
+
# Related tasks display
|
|
212
|
+
if related_tasks:
|
|
213
|
+
lines.append("")
|
|
214
|
+
lines.append("**关联任务:**")
|
|
215
|
+
lines.append("")
|
|
216
|
+
lines.append("| 关系 | 任务 | 说明 |")
|
|
217
|
+
lines.append("|------|------|------|")
|
|
218
|
+
for rt in related_tasks:
|
|
219
|
+
t = rt['type']
|
|
220
|
+
p = rt['task_path']
|
|
221
|
+
d = rt['description']
|
|
222
|
+
lines.append(f"| {t} | `{p}` | {d} |")
|
|
223
|
+
|
|
224
|
+
# Related tickets display
|
|
225
|
+
if related_tickets:
|
|
226
|
+
lines.append("")
|
|
227
|
+
lines.append("**关联 Ticket:**")
|
|
228
|
+
for rti in related_tickets:
|
|
229
|
+
tid = rti['ticket_id']
|
|
230
|
+
td = rti['description']
|
|
231
|
+
lines.append(f"- #{tid} — {td}")
|
|
232
|
+
|
|
233
|
+
if checklist:
|
|
234
|
+
done = sum(1 for c in checklist if c.startswith('- [x]'))
|
|
235
|
+
total = len(checklist)
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append(f"### Checklist ({done}/{total} done)")
|
|
238
|
+
for item in checklist:
|
|
239
|
+
lines.append(f"- {item}")
|
|
240
|
+
|
|
241
|
+
if notes:
|
|
242
|
+
lines.append("")
|
|
243
|
+
lines.append("### Notes")
|
|
244
|
+
for n in notes[:5]:
|
|
245
|
+
lines.append(f"- {n}")
|
|
246
|
+
|
|
247
|
+
lines.extend(["", "---", ""])
|
|
248
|
+
return '\n'.join(lines)
|
|
249
|
+
|
|
250
|
+
def get_inbox(tasks_root):
|
|
251
|
+
inbox_dir = os.path.join(tasks_root, "inbox")
|
|
252
|
+
items = []
|
|
253
|
+
if os.path.isdir(inbox_dir):
|
|
254
|
+
items = [f for f in os.listdir(inbox_dir)
|
|
255
|
+
if os.path.isfile(os.path.join(inbox_dir, f)) or os.path.isdir(os.path.join(inbox_dir, f))]
|
|
256
|
+
return sorted(items)
|
|
257
|
+
|
|
258
|
+
def write_readme(tasks_root):
|
|
259
|
+
tasks = gather_tasks(tasks_root)
|
|
260
|
+
content = gen_readme(tasks)
|
|
261
|
+
path = os.path.join(tasks_root, "README.md")
|
|
262
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
263
|
+
f.write(content)
|
|
264
|
+
return path, len(content)
|
|
265
|
+
|
|
266
|
+
def write_tasks_md(tasks_root):
|
|
267
|
+
tasks = gather_tasks(tasks_root)
|
|
268
|
+
content = gen_tasks_md(tasks)
|
|
269
|
+
path = os.path.join(tasks_root, "TASKS.md")
|
|
270
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
271
|
+
f.write(content)
|
|
272
|
+
return path, len(content)
|
|
273
|
+
|
|
274
|
+
def main():
|
|
275
|
+
tasks_root = TASKS_ROOT
|
|
276
|
+
for i, arg in enumerate(sys.argv[1:]):
|
|
277
|
+
if arg == '--tasks-dir' and i+2 < len(sys.argv):
|
|
278
|
+
tasks_root = os.path.abspath(sys.argv[i+2])
|
|
279
|
+
|
|
280
|
+
if not os.path.isdir(tasks_root):
|
|
281
|
+
print(f"Error: tasks root not found: {tasks_root}", file=sys.stderr)
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
p1, s1 = write_readme(tasks_root)
|
|
285
|
+
p2, s2 = write_tasks_md(tasks_root)
|
|
286
|
+
tasks = gather_tasks(tasks_root)
|
|
287
|
+
print(f"Updated:")
|
|
288
|
+
print(f" {p1} ({s1}B)")
|
|
289
|
+
print(f" {p2} ({s2}B)")
|
|
290
|
+
print(f" Found {len(tasks)} active tasks")
|
|
291
|
+
|
|
292
|
+
if __name__ == '__main__':
|
|
293
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Task: <Name>
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
TASK.md 关键字段规范(所有 task 遵守):
|
|
5
|
+
- # Task: <Name> — 标题(英文)
|
|
6
|
+
- ## Status — 状态(英文)
|
|
7
|
+
- ## Goal — 目标(英文)
|
|
8
|
+
- ## Checklist — 步骤清单(英文)
|
|
9
|
+
- ## Notes — 备注(英文)
|
|
10
|
+
- ## Related Tasks — 关联任务关系表(可选)
|
|
11
|
+
脚本 update-index.py 只解析以上英文关键字段。
|
|
12
|
+
中文字段(## 状态、## 目标、## 步骤)为向下兼容而支持,新 task 请用英文。
|
|
13
|
+
-->
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
active — <brief description of current work>
|
|
18
|
+
|
|
19
|
+
## Goal
|
|
20
|
+
|
|
21
|
+
<one-liner>
|
|
22
|
+
|
|
23
|
+
## Skills (用于跨 skill 组合任务)
|
|
24
|
+
|
|
25
|
+
- `<skill-name>` — <该 skill 在本任务中的用途>
|
|
26
|
+
|
|
27
|
+
## Repo
|
|
28
|
+
|
|
29
|
+
<!-- 可选字段。若任务关联独立的项目仓库,在此声明路径。
|
|
30
|
+
所有源码级产出(设计文档、代码、配置、测试)放到此仓库中,
|
|
31
|
+
而非 task/output/。执行日志和调试信息仍放在 task/output/logs/。
|
|
32
|
+
没有此字段时,所有产出放 task/output/(默认行为)。 -->
|
|
33
|
+
|
|
34
|
+
路径: `<绝对路径,如 ~/studio/hermes/projects/agora/>`
|
|
35
|
+
用途: <一句话描述>
|
|
36
|
+
|
|
37
|
+
## Affinity (cluster task)
|
|
38
|
+
|
|
39
|
+
<!-- 控制此任务被哪个节点认领。影响 .hermes-task.json 的 affinity 字段。
|
|
40
|
+
不带 cluster 时此字段无任何效果,保留无害。 -->
|
|
41
|
+
|
|
42
|
+
- `any` — 任何节点可认领
|
|
43
|
+
- `<hash6>` — 指定节点(推荐;跨机器同步安全)
|
|
44
|
+
- `<suite-name>` — 拥有此套件的节点(如 `hermes-personal-suite`)
|
|
45
|
+
- `<tag>` — 能力标签匹配(如 `gpu`, `docker`)
|
|
46
|
+
- 逗号分隔 → AND 逻辑(`hermes-personal-suite,gpu` 两样都有才匹配)
|
|
47
|
+
|
|
48
|
+
注意:不要用 `local`。tasks repo 同步到其他机器后,每台都会把 `local` 当成自己的任务,造成混乱。用 node_id(hash6)代替。
|
|
49
|
+
|
|
50
|
+
## 环境要求
|
|
51
|
+
|
|
52
|
+
| 项目 | 最低要求 | 说明 |
|
|
53
|
+
|------|---------|------|
|
|
54
|
+
| Python | <版本> | <说明> |
|
|
55
|
+
| <依赖项> | <版本> | <说明> |
|
|
56
|
+
| 磁盘 | <空闲空间> | <用途> |
|
|
57
|
+
| API Key | <Key 名称> | <用途说明> |
|
|
58
|
+
|
|
59
|
+
## Data Flow
|
|
60
|
+
|
|
61
|
+
跨 phase 文件传递路径。phase 执行时 CWD 在自己的子目录,跨目录引用使用相对路径(`../`)。
|
|
62
|
+
|
|
63
|
+
| 文件 | 来源 Phase | 被消费 Phase | 格式说明 |
|
|
64
|
+
|------|-----------|-------------|---------|
|
|
65
|
+
| `phase_01_<skill>/<file>` | Phase 1 | Phase N | <格式文档> |
|
|
66
|
+
| `phase_02_<skill>/<file>` | Phase 2 | Phase N | <格式文档> |
|
|
67
|
+
|
|
68
|
+
## Phase 目录
|
|
69
|
+
|
|
70
|
+
- `phase_01_<skill>/` — <产出物说明>
|
|
71
|
+
- `phase_02_<skill>/` — <产出物说明>
|
|
72
|
+
- `phase_03_<skill>/` — <产出物说明>
|
|
73
|
+
|
|
74
|
+
## Checklist
|
|
75
|
+
|
|
76
|
+
- [ ] Phase 1 — <investigation / reading / understanding>
|
|
77
|
+
- [ ] Phase 2 — <execution / writing / implementation>
|
|
78
|
+
- [ ] BREAK: <optional — delete if no pause needed here>
|
|
79
|
+
- [ ] Phase 3 — <verification / review / cleanup>
|
|
80
|
+
|
|
81
|
+
## Tracking
|
|
82
|
+
|
|
83
|
+
<!-- 可选。每个 phase 完成后需要更新的跟踪记录。
|
|
84
|
+
编排者(stratis/corvan/valros)读到本节后,自动注入到各 phase 的 card/worker brief 中。 -->
|
|
85
|
+
|
|
86
|
+
加载 `task-tracker` skill,传入以下参数:
|
|
87
|
+
|
|
88
|
+
| 参数 | 值 | 说明 |
|
|
89
|
+
|------|-----|------|
|
|
90
|
+
| `task_dir` | 本 TASK.md 所在目录的绝对路径 | 让 tracker 定位任务 |
|
|
91
|
+
| `phase` | 当前完成的 phase 标识 | 如 Phase 0: scrutiny-pipeline |
|
|
92
|
+
| `executor` | 执行者名称 | 如 scrutaris |
|
|
93
|
+
| `findings` | 关键发现(每行一条) | 从产出中提炼 3-5 条 |
|
|
94
|
+
| `outputs` | 产出文件路径(相对 task_dir) | 如 output/docs/00-report.md |
|
|
95
|
+
| `next` | 下一个 phase | 如 Phase 1: domain-analysis |
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
## Related Tickets
|
|
100
|
+
|
|
101
|
+
<!-- 可选。关联的 ticket。填写 ticket_id 后 task_create 会自动将 task hash 写回 ticket 的 task_id 字段。 -->
|
|
102
|
+
|
|
103
|
+
| Ticket ID | 关系说明 |
|
|
104
|
+
|-----------|---------|
|
|
105
|
+
| `#<ticket_id>` | 创建源 / 相关事项 |
|
|
106
|
+
|
|
107
|
+
## Related Tasks
|
|
108
|
+
|
|
109
|
+
<!-- 可选。关联任务关系表:depends_on = 前置依赖(本任务开始前必须完成),related = 主题相关/互为参考,supersedes = 本任务替代/废弃了哪个旧任务 -->
|
|
110
|
+
| 类型 | 关联 Task | 关系说明 |
|
|
111
|
+
|------|----------|---------|
|
|
112
|
+
| depends_on | `YYYYMMDD-HHMMSS.<name>-<hash6>/` | 本任务依赖的前置任务 |
|
|
113
|
+
| related | `YYYYMMDD-HHMMSS.<name>-<hash6>/` | 主题相关、互为参考 |
|
|
114
|
+
| supersedes | `YYYYMMDD-HHMMSS.<name>-<hash6>/` | 本任务替代/废弃了哪个旧任务 |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# TASK_MEMORY.md — 任务操作记忆
|
|
2
|
+
|
|
3
|
+
按时间倒序(最新在最上面)或正序(最新在最下面)追加。推荐正序,方便跨 session 顺读。
|
|
4
|
+
|
|
5
|
+
## 2026-06-06 HH:MM
|
|
6
|
+
|
|
7
|
+
**操作:** 做了什么
|
|
8
|
+
**原因:** 为什么这么做
|
|
9
|
+
**改动:** 改了什么文件/目录,具体改动
|
|
10
|
+
**问题:** 遇到的问题
|
|
11
|
+
**解决:** 怎么解决的
|
|
12
|
+
**结果:** 最终结果
|
|
13
|
+
**素材状态:** phase-dir(内容✅/⚠️/❌) ...
|
|
14
|
+
**下一步:** 下次 session 要继续做什么
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 记录规范
|
|
19
|
+
|
|
20
|
+
- 每个 session(或每次连续操作)写一条 `## 日期 时间` 记录
|
|
21
|
+
- **操作:** 必填,一句话概括
|
|
22
|
+
- **原因:** 可选,决策理由
|
|
23
|
+
- **改动/问题/解决/结果:** 按需填写
|
|
24
|
+
- **素材状态:** 推荐维护,快速知道什么能复用
|
|
25
|
+
- **下一步:** 跨 session 必填,新 session 的起点
|
|
26
|
+
- 纯命令输出不要写这里(放 `logs/`)
|
|
27
|
+
- checklist 勾选不要写这里(放 `TASK.md`)
|